@lovek4d/kickflip 0.5.0 → 0.5.3

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 (76) hide show
  1. package/CONTEXT.md +47 -42
  2. package/README.md +1 -1
  3. package/SKILL.md +16 -16
  4. package/bin/kickflip.mjs +26 -17
  5. package/bin/wait-events.mjs +2 -2
  6. package/board/agents.mjs +2 -0
  7. package/board/bin/board.mjs +65 -18
  8. package/board/domain.mjs +7 -7
  9. package/board/finish.mjs +4 -4
  10. package/board/headless.mjs +5 -4
  11. package/board/on-host.mjs +1 -1
  12. package/board/public/app-asks.js +6 -6
  13. package/board/public/app-banner.js +7 -13
  14. package/board/public/app-cards.js +51 -27
  15. package/board/public/app-core.js +26 -8
  16. package/board/public/app-header.js +2 -1
  17. package/board/public/app-lanes.js +55 -23
  18. package/board/public/app-needs.js +129 -4
  19. package/board/public/app-reader.js +1 -1
  20. package/board/public/app-snapshot.js +6 -5
  21. package/board/public/app-threads.js +70 -59
  22. package/board/public/app.js +38 -8
  23. package/board/public/attach.js +2 -2
  24. package/board/public/dialogs.js +9 -39
  25. package/board/public/hub-kit.js +4 -2
  26. package/board/public/linear-key.js +46 -35
  27. package/board/public/new-task.js +17 -38
  28. package/board/public/prototype-kit.js +28 -3
  29. package/board/public/prototype-switcher.js +42 -107
  30. package/board/public/shell.js +7 -5
  31. package/board/public/sidebar.js +44 -5
  32. package/board/public/style.css +108 -37
  33. package/board/public/ui-cards.js +41 -30
  34. package/board/public/ui-header.js +2 -2
  35. package/board/public/ui-order.js +12 -10
  36. package/board/public/ui-questions.js +25 -38
  37. package/board/public/ui-sidebar.js +18 -12
  38. package/board/public/ui-threads.js +3 -3
  39. package/board/public/ui.js +40 -8
  40. package/board/public/view-hub-settings.js +11 -10
  41. package/board/public/view-hub.js +17 -3
  42. package/board/public/view-questions.js +142 -199
  43. package/board/public/view-settings.js +191 -72
  44. package/board/server.mjs +19 -20
  45. package/board/store.mjs +20 -15
  46. package/board/transcript.mjs +2 -0
  47. package/board/worktree.mjs +24 -20
  48. package/hooks-plugin/hooks/board-data-gate.mjs +50 -0
  49. package/hooks-plugin/hooks/hooks.json +9 -0
  50. package/hub/boards.mjs +4 -4
  51. package/hub/front-door.mjs +5 -5
  52. package/hub/github.mjs +3 -3
  53. package/hub/hub.mjs +11 -11
  54. package/hub/linear.mjs +1 -2
  55. package/hub/live.mjs +2 -2
  56. package/hub/locks.mjs +1 -1
  57. package/hub/pr-report.mjs +2 -2
  58. package/hub/presence.mjs +1 -1
  59. package/hub/push.mjs +2 -2
  60. package/hub/readiness.mjs +1 -2
  61. package/hub/review-send.mjs +1 -0
  62. package/hub/secrets.mjs +2 -0
  63. package/hub/self-restart.mjs +2 -0
  64. package/hub/sessions.mjs +2 -2
  65. package/hub/share.mjs +1 -0
  66. package/hub/slack.mjs +2 -2
  67. package/hub/stream.mjs +10 -13
  68. package/hub/try.mjs +6 -6
  69. package/hub/upgrade.mjs +1 -1
  70. package/hub/usage.mjs +3 -3
  71. package/hub/views.mjs +1 -1
  72. package/package.json +2 -1
  73. package/shared/audit.mjs +4 -0
  74. package/shared/fake-gh.mjs +17 -4
  75. package/shared/service.mjs +4 -4
  76. package/shared/usage-gate.mjs +1 -1
package/hub/hub.mjs CHANGED
@@ -1,16 +1,16 @@
1
1
  #!/usr/bin/env node
2
- var Rt=Object.defineProperty;var l=(e,t)=>Rt(e,"name",{value:t,configurable:!0});import Re from"node:http";import Et from"node:net";import{spawn as Ee,execFileSync as Dt}from"node:child_process";import w from"node:fs";import $ from"node:path";import De from"node:os";import{fileURLToPath as Lt}from"node:url";import Le from"node:vm";import Ct from"node:zlib";import{routeRecord as j}from"./routes.mjs";import{publicKey as At,addSubscription as Ht,removeSubscription as It,sendAll as Mt,flushQueue as Ce,queueStatus as Ae,saveSettings as Bt,readSettings as V,migrateSettings as Ft,setNotifications as Ut,readCodexShare as He,setCodexShare as Nt,readClaudeModel as Gt,setClaudeModel as zt,deliver as Wt,pushRoute as Jt}from"./push.mjs";import{ownerTracker as Kt}from"./presence.mjs";import{POLL_MS as Ie,answerOf as Vt,expired as Zt,pendingFor as Me,promptRecord as Qt,pushOf as Xt,updatePrompts as M,shown as Yt,pruneRemembered as fe,rememberRules as qt,rememberedAnswer as en,rememberedFor as Be}from"./prompts.mjs";import{hold as tn,heldUntil as nn,holdLine as an,editQuiet as sn,checkEvery as Fe,nextCheckAt as rn,LOW_POINTS as on}from"./schedule.mjs";import{capital as cn,hubView as un,settingsView as ln,flushView as dn,SNOOZE_REFUSED as hn,AWAKE_REFUSED as fn}from"./views.mjs";import{caffeinate as pn,setKeepAwake as mn,awakeUntil as Ue,awakeUnsupported as gn}from"./awake.mjs";import{checkGitHub as wn,sendWakes as bn}from"./github.mjs";import{checkIncoming as yn}from"./incoming.mjs";import{listBoards as R,isListening as Sn,isBoardSite as Ne,listenState as Ge,RESUME_STATE as kn,sendToBoards as ze,checkListening as $n,recordHubCode as _n,runningHubCode as xn,boardCode as We,recordLinkFaults as vn,linksOf as Tn,hubLink as On,hubCodeState as jn,readJSON as Pn}from"./boards.mjs";import{liveSessions as Je}from"./live.mjs";import{RuleError as Rn,applyRules as En,changed as Ke,readHubRules as pe,writeHubRules as Dn}from"./rules.mjs";import{siteIcon as Ln}from"./icons.mjs";import{readNeeds as Cn,sidebarSites as An,startRoute as Hn,hubHealth as In}from"./home.mjs";import{checkSessions as Mn,compactFullContexts as Bn,startingBoards as Fn}from"./sessions.mjs";import{repoService as Un,repoLookupRoute as Nn}from"./repos.mjs";import{boardRepo as Ve,changeBoardRepo as Gn,migrateBoardRepos as zn,migrateHubBoard as Wn}from"./board-repo.mjs";import{requestBoard as Jn,retryBoard as Kn,dismissBoard as Vn,clonePercent as Zn,settleStarts as Qn}from"./new-board.mjs";import{boardClosure as Xn,closeBoard as Yn,closedBoards as qn,reopenBoard as ea}from"./close-board.mjs";import{restartBoard as ta,restartable as na}from"./restart-board.mjs";import{crossSite as aa,isJSON as me}from"../starter/cross-site.mjs";import{BOARD_PUBLIC as ge,appFile as sa,colorsVersion as Ze,destOf as ra,fileVersion as oa,pageOf as Qe,shellHtml as ia}from"./shell.mjs";import{hubStream as ca}from"./stream.mjs";import{usageReader as ua,checkUsagePause as la,contextReading as da}from"./usage.mjs";import{readUsageGate as Xe,setPauseAt as ha,setKeepGoing as fa}from"../shared/usage-gate.mjs";import{readiness as pa,readCodex as Z}from"./readiness.mjs";import{BuildError as ma,startBuild as ga,stopTries as wa,tryTick as we,initializeTries as ba,fetchTryBranches as Ye,pullNow as ya,pullMains as Sa}from"./try.mjs";import{serveFiles as ka}from"./files.mjs";import{upgrader as $a,CHECK_MS as _a}from"./upgrade.mjs";import{fallBackFromLinear as xa,openBoardBuilders as qe}from"../board/store.mjs";import{recordRefusal as va}from"../board/permissions.mjs";import{hubGate as et}from"../board/on-host.mjs";import{pipeUpgrade as Ta,refuse as Oa,refuseUpgrade as ja}from"./front-door.mjs";import{restartRunner as Pa}from"../board/worktree.mjs";import{hubService as Ra}from"../shared/service.mjs";import{cloneRoot as tt}from"../shared/repos.mjs";import{LinearError as be,linearStatus as Q,removeLinearKey as Ea,saveLinearKey as Da,savedClient as nt}from"./linear.mjs";const X=$.dirname(Lt(import.meta.url)),La=$.join(X,"..","shared","colors.css"),c=l((...e)=>console.log(new Date().toISOString(),...e),"log"),Y=$.join(De.homedir(),".claude","kickflip"),at=$.join(Y,"manifest.json"),q=$.join(Y,"logs"),st=$.join(Y,"run"),ye=Number(process.env.KICKFLIP_HUB_PORT||4500);for(const e of[Y,q,st])w.mkdirSync(e,{recursive:!0});try{const e=_n();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 zn();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 Wn();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 L=Un(),Ca=ua(),ee=pn({log:c});ee.apply();let te={version:1,sites:{}};function g(){try{te=JSON.parse(w.readFileSync(at,"utf8")),te.sites||={}}catch(e){e.code==="ENOENT"?te={version:1,sites:{}}:c("manifest unreadable, keeping the last good copy:",e.message)}return te}l(g,"loadManifest");const O=new Map,C=new Set;let ne=!1;const rt=5e3,ae=l(e=>JSON.stringify([e.cmd,e.cwd||e.dir,e.port,e.env||{},e.restart_at||0]),"keyOf"),Se=l(e=>Array.isArray(e?.cmd)&&e.cmd.length>0&&!!e.port,"runs"),B=l(e=>Se(e)?"cmd":e?.port?"port":"files","servesOf"),F=new Map;function se(e){return $.join(st,e+".pid")}l(se,"pidFile");function Aa(e,t){try{const n=Number(w.readFileSync(se(e),"utf8")),r=n?Dt("ps",["-o","command=","-p",String(n)],{encoding:"utf8"}).trim():"";r&&r.includes(t.cmd[t.cmd.length-1])&&(process.kill(-n,"SIGTERM"),c(`stopped stale ${e} (pgid ${n})`))}catch{}try{w.unlinkSync(se(e))}catch{}}l(Aa,"killStale");function Ha(e,t){const n=O.get(e)||{restarts:0},r=w.openSync($.join(q,e+".log"),"a"),[a,...d]=t.cmd,h={...process.env,...t.env||{},PORT:String(t.port),PREFIX:"/"+e,KICKFLIP_ROUTE:e};w.writeSync(r,`
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,`
3
3
  --- ${new Date().toISOString()} hub starting: ${t.cmd.join(" ")} (port ${t.port})
4
- `),n.code=Ne(t)?We(t):null;let i;try{i=Ee(a,d,{cwd:t.cwd||t.dir,env:h,stdio:["ignore",r,r],detached:!0})}catch(k){w.writeSync(r,`hub could not start it: ${k.message}
5
- `),w.closeSync(r),Object.assign(n,{child:null,key:ae(t),state:"crashed",exitCode:null,nextStart:Date.now()+ot(n.restarts++)}),O.set(e,n);return}w.closeSync(r),w.writeFileSync(se(e),String(i.pid)),Object.assign(n,{child:i,key:ae(t),state:"starting",since:Date.now(),exitCode:null,up:!1}),O.set(e,n),c(`started ${e} pid ${i.pid}`),i.pid&&C.add(i);const S=l((k,y)=>{if(C.delete(i),n.child!==i)return;try{w.unlinkSync(se(e))}catch{}Date.now()-n.since>6e4&&(n.restarts=0),Object.assign(n,{child:null,state:n.stopping?"stopped":"crashed",exitCode:k??y,nextStart:Date.now()+ot(n.restarts++),why:n.stopping?n.why:void 0}),n.stopping=!1,c(`${e} exited (${k??y})`)},"ended");i.on("error",k=>{c(`${e} spawn error: ${k.message}`),i.pid||S(k.code,null)}),i.on("exit",S)}l(Ha,"start");const ot=l(e=>Math.min(6e4,1e3*2**Math.min(e,6)),"backoff");function re(e){const t=O.get(e);if(!t||!t.child){O.delete(e);return}t.stopping=!0;const n=t.child;oe(n,"SIGTERM"),setTimeout(()=>{C.has(n)&&oe(n,"SIGKILL")},rt)}l(re,"stop");const oe=l((e,t)=>{try{process.kill(-e.pid,t)}catch{}},"signal");function ke(e){return new Promise(t=>{const n=Et.connect({port:e,host:"127.0.0.1"}),r=l(a=>{n.destroy(),t(a)},"done");n.setTimeout(800,()=>r(!1)),n.on("connect",()=>r(!0)),n.on("error",()=>r(!1))})}l(ke,"probe");let it=!0;const A=new Set;let $e=!1;const U=new Set,Ia=3e4;async function Ma(e){const t=Date.now()+Ia;for(;;){const n=j(g().sites,e),r=O.get(e);if(n&&r?.child&&r.up&&r.key===ae(n))return!0;if(Date.now()>=t)return!1;await new Promise(a=>setTimeout(a,250))}}l(Ma,"serverAnswers");async function ct(){if(ne)return;const{sites:e}=g();for(const t of[...O.keys()])A.has(t)||(j(e,t)?Se(j(e,t))||(c(`${t} no longer has a command; stopping it`),re(t),O.delete(t)):(c(`${t} left the manifest; stopping it`),re(t),O.delete(t)));for(const t of[...F.keys()])B(j(e,t))!=="port"&&F.delete(t);for(const[t,n]of Object.entries(e))B(n)==="port"&&F.set(t,await ke(n.port));for(const[t,n]of Object.entries(e)){if(!Se(n))continue;it&&Aa(t,n);const r=O.get(t);if(r&&r.child&&r.key!==ae(n)){c(`${t} entry changed; restarting it`),re(t),r.restarts=0,r.nextStart=Date.now()+1500;continue}const a=r?.child&&r.code&&!r.stopping&&!U.has(t)&&!A.has(t)?We(n):null;if(a&&a!==r.code){if(!(await qe(n.dir)).length){c(`${t} runs older code than its files; restarting it`),re(t),Object.assign(r,{restarts:0,nextStart:Date.now()+1500,why:"code"});continue}r.waitedOn!==a&&c(`${t} runs older code than its files; restarts once no builder runs`),r.waitedOn=a}(!r||!r.child&&Date.now()>=(r.nextStart||0))&&Ha(t,n)}it=!1;for(const[t,n]of O){const r=j(e,t);r&&(n.up=n.child?await ke(r.port):!1,n.child&&n.up&&Object.assign(n,{state:"up"},n.stopping?{}:{why:void 0}))}}l(ct,"reconcile");const Ba=setInterval(()=>ct().catch(e=>c("reconcile failed:",e.message)),2e3);ct().catch(e=>c("reconcile failed:",e.message));try{Ft()&&c("settings: migrated the polling windows to quiet hours")}catch(e){c("settings migration failed:",e.message)}const ut=l(async()=>{try{for(const e of await xa())c(`linear: ${e}`)}catch(e){c("linear fallback failed:",e.message)}},"linearFallback");await ut();const Fa=6e4,lt=60*6e4,b={running:null,startedAt:0,firstAt:Date.now()+Fa,last:null,remaining:null,errorLoggedAt:0,linearErrorLoggedAt:0},H=Kt(),dt=l(()=>b.startedAt?rn({last:new Date(b.startedAt),...H.state(),remaining:b.remaining}):new Date(b.firstAt),"nextPollAt");function ht({changed:e,back:t}){e&&_.refresh(),t&&b.startedAt&&!b.running&&_e().catch(n=>c("poll failed:",n.message))}l(ht,"presenceChanged");function _e({flush:e=!0}={}){if(b.running)return b.running;const t=new Date;return b.startedAt=t.getTime(),b.running=(async()=>{const n=V();let r;try{r=await wn({every:Fe({present:!1,remaining:b.remaining}),manifest:g()})}catch(a){r={at:t.toISOString(),watched:0,failed:[a.message],changes:[],events:[],checked:[],merged:[],linear:{moved:[],failed:[]},rate:null}}b.remaining=r.rate?.remaining??null,r.merged.length&&(await Ye({manifest:g(),log:c,routes:r.merged}),c(`merged on ${r.merged.join(", ")}: fetched and refreshing Main`),we({manifest:g(),routes:r.merged}).catch(a=>c("merge refresh failed:",a.message))),await Ye({manifest:g(),log:c});try{const a=await Sa({manifest:g()});a.length&&we({manifest:g(),routes:a}).catch(d=>c("pull refresh failed:",d.message))}catch(a){c("pull failed:",a.message)}try{r.failed.push(...(await yn({manifest:g()})).failed)}catch(a){r.failed.push(`incoming PRs: ${a.message}`)}r.failed.length&&Date.now()-b.errorLoggedAt>lt&&(b.errorLoggedAt=Date.now(),c(`github check: ${r.failed.length} step${r.failed.length===1?"":"s"} failed, first: ${r.failed[0]} (logged at most hourly)`)),r.changes.length&&c(`github check: ${r.changes.map(a=>`#${a.pr} ${a.changes.join(",")}`).join("; ")} \u2192 ${r.events.map(a=>a.route).join(", ")||"no board"}`);for(const{id:a,state:d}of r.linear.moved)c(`linear: ${a} \u2192 ${d}`);r.linear.failed.length&&Date.now()-b.linearErrorLoggedAt>lt&&(b.linearErrorLoggedAt=Date.now(),c(`linear check: ${r.linear.failed.length} step${r.linear.failed.length===1?"":"s"} failed, first: ${r.linear.failed[0]} (logged at most hourly)`));try{r.deaf=await $n({manifest:g()})}catch(a){r.deaf={pushed:[],failed:[a.message]}}(r.deaf.pushed.length||r.deaf.failed.length)&&c(`not listening: pushed ${r.deaf.pushed.join(", ")||"none"}${r.deaf.failed.length?`; ${r.deaf.failed.join("; ")}`:""}`);try{vn(g())}catch(a){c("could not record link faults:",a.message)}if(r.flushed=null,r.held=nn(t,n)!==null,e&&!r.held)try{const a=await Ce();a&&(r.flushed={count:a.count,sent:a.sent,ok:a.ok},c(`push queue: sent ${a.sent} of ${a.count} queued`))}catch(a){c("push queue flush failed:",a.message)}return b.last=r,r})().finally(()=>{b.running=null}),b.running}l(_e,"runPoll");let ft=null;function Ua(){ft||=bn({settings:V(),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(()=>{ft=null})}l(Ua,"runWakes");const xe=new Set,D={running:null};function pt(){if(D.running||A.size||U.size)return;const e=new Set(xe);xe.clear(),D.running=Mn({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(()=>Bn({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(()=>{D.running=null})}l(pt,"runSessionCheck"),await ba(g());const Na=Date.now(),N=new Map;function G(e){e?(ce.delete(e),_.changed(e)):_.refresh()}l(G,"promptChanged");async function mt(){const{gone:e,rulesChanged:t}=await M(n=>{const r=Zt(n,{now:Date.now(),aliveSince:Na}),a=fe(n,ve());for(const d of r)delete n.prompts[d.id];return{gone:r,rulesChanged:a}});for(const n of e)G(n.route);t&&_.refresh(),e.length&&c(`prompts: dropped ${e.map(n=>n.id).join(", ")}, whose hook went away`)}l(mt,"prunePrompts");const ve=l(()=>({running:Je(),boards:R(g())}),"promptContext");async function gt(e){const t=R(g()).find(n=>n.owner?.session&&n.owner.session===e.session_id);if(!t)return null;try{const n=await va(Ve(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(gt,"noteRefusal");let z=null,wt=null;async function Te(){return z?wt??=z.catch(()=>{}).then(()=>(wt=null,Te())):(z=(async()=>la({gate:Xe(),resume:l(async e=>{const t=await ze(R(g()),"usage-resumed",{window:e});for(const n of t.failed)c(n)},"resume")}))().finally(()=>{z=null}),z)}l(Te,"checkUsageGate"),setInterval(()=>{we({manifest:g()}).catch(e=>c("try tick failed:",e.message)),W.tick().catch(e=>c("kickflip update tick failed:",e.message)),Te().catch(e=>c("usage gate failed:",e.message)),H.check().then(ht,e=>c("presence check failed:",e.message)).finally(()=>{!b.running&&Date.now()>=dt().getTime()&&_e().catch(e=>c("poll failed:",e.message))}),Qn({listening:l(e=>Sn(e)&&ie().some(t=>t.route===e.route&&t.state==="up"),"listening")}).then(e=>{e.length&&_.refresh()},e=>c("settling starts failed:",e.message)),Ua(),ee.apply(),mt().catch(e=>c("prompt expiry failed:",e.message)).finally(pt)},15e3);function bt(){if(ne)return;ne=!0,clearInterval(Ba),ee.stop(),c("hub stopping; stopping sites");for(const n of O.values())n.child&&(n.stopping=!0);for(const n of C)oe(n,"SIGTERM");let e=!1;wa().finally(()=>{e=!0});const t=Date.now()+rt;setInterval(()=>{if(!((!e||C.size)&&Date.now()<t)){for(const n of C)c(`pid ${n.pid} ignored SIGTERM; killing it`),oe(n,"SIGKILL");process.exit(0)}},50)}l(bt,"shutdown"),process.on("SIGTERM",bt),process.on("SIGINT",bt);const yt=new Map;function St(e,t={n:3e3}){let n=0,r;try{r=w.readdirSync(e,{withFileTypes:!0})}catch{return 0}for(const a of r){if(t.n--<=0)break;if(a.name.startsWith(".")||a.name==="node_modules")continue;const d=$.join(e,a.name);if(a.isDirectory())n=Math.max(n,St(d,t));else try{n=Math.max(n,w.statSync(d).mtimeMs)}catch{}}return n}l(St,"newestMtime");function Ga(e,t){const n=yt.get(e);if(n&&Date.now()-n.checked<3e4)return n.at;const r=St(t.dir);return yt.set(e,{at:r,checked:Date.now()}),r}l(Ga,"changedAt");const kt=l(e=>{try{return typeof e=="string"&&w.statSync(e).isDirectory()}catch{return!1}},"isFolder");function ie(){const{sites:e}=g();return Object.entries(e).sort(([t],[n])=>t.localeCompare(n)).map(([t,n])=>{const r=O.get(t)||{},a=B(n),d=a==="files"?kt(n.dir)?"up":"down":a==="port"?F.has(t)?F.get(t)?"up":"down":"starting":r.child?r.up?"up":"starting":r.state==="crashed"?"down":r.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:a,state:d,exitCode:r.exitCode??null,owner:n.owner||null,restart:na(n),changedAt:Ga(t,n)||null,created:n.created||null,log:$.join(q,t+".log")}})}l(ie,"status");const I=l(e=>String(e??"").replace(/[&<>"']/g,t=>({"&":"&amp;","<":"&lt;",">":"&gt;",'"':"&quot;","'":"&#39;"})[t]),"esc"),ce=new Map,$t=2e4,za=1500;async function Wa(e){try{const t=await fetch(`http://127.0.0.1:${e}/_claude/needs`,{signal:AbortSignal.timeout(za)});return t.ok?Cn(await t.json()):null}catch{return null}}l(Wa,"fetchNeeds");async function _t(e,{fresh:t=!1}={}){const n=e.filter(a=>a.state==="up"&&a.owner&&a.port);for(const a of n){const d=ce.get(a.route);(t===!0||t?.has?.(a.route)||!d||Date.now()-d.at>=$t)&&ce.set(a.route,{at:Date.now(),needs:Wa(a.port)})}const r=await Promise.all(n.map(a=>ce.get(a.route).needs));return new Map(n.map((a,d)=>[a.route,r[d]]))}l(_t,"waitingCounts");const xt=l(e=>[...e.values()].reduce((t,n)=>t+(n?.count||0),0)+Me(null).length,"badgeTotal"),Oe=new Map;function Ja(e){const t=new Map,n=new Map,r={running:Je(),wakes:Pn(kn,{})};for(const a of e)try{const d=Ge(a,r);n.set(a.route,d);let h=Oe.get(a.route);(!h||h.listen!==d||Date.now()-h.at>=$t)&&(h={at:Date.now(),listen:d,links:Tn(a,{listen:d})},Oe.set(a.route,h)),t.set(a.route,h.links)}catch(d){c(`links of ${a.route} failed:`,d.message),t.set(a.route,[])}return{links:t,listens:n}}l(Ja,"boardLinks");async function ue({fresh:e=!1}={}){const t=new Date,n=ie(),r=await _t(n,{fresh:e}),a=g(),d=R(a),{links:h,listens:i}=Ja(d),S=jn(),k=new Map(d.map(u=>[u.route,da(u.owner?.session)]).filter(([,u])=>u)),y=Fn(a,t.getTime(),Zn,u=>i.get(u)==="listening"&&n.some(f=>f.route===u&&f.state==="up")),m=new Set(y.map(u=>u.route)),p=An(n,r,new Set(d.map(u=>u.route)),i,k).filter(u=>!m.has(u.route)),v=V(),s=tn(t,v);return{data:{code:S.running??null,total:xt(r),start:Hn(p),sites:p,hub:In(p,h,On(S)),home:w.realpathSync(De.homedir()),starting:y,usage:Ca({codexShare:He(Z())}),pause:Xe(),upgrade:W.state(),notifications:s?s.why==="off"?{state:"off",text:"Off"}:{state:"held",text:cn(an(s,t))}:{state:"on",text:"On"},away:!H.state().present},links:h,rows:n,code:S}}l(ue,"sidebarState");function vt(e,t,n,r,a){let d=Buffer.isBuffer(a)?a:Buffer.from(a);const h={...r,vary:[r.vary,"accept-encoding"].filter(Boolean).join(", ")};d.length>=1024&&/\bgzip\b/.test(e.headers["accept-encoding"]||"")&&(d=Ct.gzipSync(d),h["content-encoding"]="gzip"),h["content-length"]=d.length,t.writeHead(n,h),t.end(e.method==="HEAD"?void 0:d)}l(vt,"sendText");async function Ka(e){const{data:t}=await ue(),n=Le.createContext({});for(const r of["ui-phase-bars.js","ui-page-header.js","ui-sidebar.js"])Le.runInContext(w.readFileSync($.join(ge,r),"utf8"),n);return ia({sidebar:n.BoardUI.sidebar(t,{here:e}),sidebarData:JSON.stringify(t).replace(/</g,"\\u003c"),touchIcon:le(180)})}l(Ka,"shellPage");const W=$a({root:tt(),builders:l(async()=>(await Promise.all(R(g()).map(e=>qe(e.dir)))).flat(),"builders"),run:Va,log:c,changed:l(()=>_.refresh(),"changed")});function Va(){const e=$.join(q,"kickflip-update.log");w.writeFileSync(e,`${new Date().toISOString()} kickflip update
6
- `);const t=w.openSync(e,"a");return c("running kickflip update"),new Promise(n=>{const r=Ee(process.execPath,[$.join(tt(),"bin","kickflip.mjs"),"update"],{detached:!0,stdio:["ignore",t,t]});w.closeSync(t),r.unref(),r.on("error",a=>n({ok:!1,error:a.message})),r.on("exit",a=>{const d=w.readFileSync(e,"utf8").trim().split(`
7
- `).at(-1);c(`kickflip update exited ${a}: ${d}`),n(a===0?{ok:!0}:{ok:!1,error:d.replace(/^kickflip: /,"")})})})}l(Va,"runUpdate"),W.check(),setInterval(()=>W.check(),_a);const _=ca({targets:l(()=>Object.entries(g().sites).filter(([,e])=>Ne(e)&&e.port).map(([e,t])=>({route:e,port:t.port})),"targets"),watched:l(()=>R(g()).map(({route:e,dir:t})=>({route:e,dir:t})),"watched"),sidebar:l(async({fresh:e})=>(await ue({fresh:e})).data,"sidebar"),log:c});function le(e){let t="0";try{t=Math.floor(w.statSync($.join(X,`icon-${e}.png`)).mtimeMs).toString(36)}catch{}return`/_hub/icon-${e}.png?v=${t}`}l(le,"iconUrl");const Za=l(()=>JSON.stringify({name:"Claude sites",short_name:"Claude",start_url:"/",scope:"/",display:"standalone",background_color:"#121514",theme_color:"#121514",icons:[{src:le(192),sizes:"192x192",type:"image/png"},{src:le(512),sizes:"512x512",type:"image/png"}]}),"manifestJson");async function E(e,t=16*1024){let n="";for await(const r of e)if(n+=r,n.length>t)throw Object.assign(new Error("too large"),{status:413});return n}l(E,"readBody");async function x(e,t){if(!me(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(x,"readJSONBody");const Tt=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>${I(e)}</title><link rel="manifest" href="/manifest.webmanifest"><link rel="icon" type="image/png" href="/_hub/kickflip.png"><link rel="apple-touch-icon" href="${le(180)}">
9
- <meta name="apple-mobile-web-app-title" content="Claude">${t}<link rel="stylesheet" href="/_hub/colors.css?v=${Ze()}"><link rel="stylesheet" href="/_hub/hub.css"></head><body><main>`,"head"),Qa=l(e=>/\bapplication\/json\b/.test(e.headers.accept||""),"wantsJSON"),Xa=l(()=>({running:!!b.running,nextAt:dt(),last:b.last,present:H.state().present,every:Fe({present:H.state().present,remaining:b.remaining}),low:b.remaining!==null&&b.remaining<on}),"pollState");async function de(e){await mt();const t=new Date,{data:n,links:r,rows:a,code:d}=await ue();return{...un({now:t,st:Ae(t),poll:Xa(),sidebar:n,rows:a,links:r,rules:pe(),integrations:await pa.rows(),code:d,host:e.headers["x-forwarded-host"]||e.headers.host||"",codexShare:He(Z()),claudeModel:Gt(),codex:Z(),awake:Ue(t),remembered:new Map(R(g()).map(h=>[h.route,Be(h.route)]))}),prompts:Me(null).map(Yt),remembered:Be(null)}}l(de,"hubData");const J=l(async()=>{const e=new Date,t={until:Ue(e),unsupported:gn()};return{...ln({now:e,st:Ae(e),rules:pe(),route:await Jt(),linear:Q(),awake:t}),closed:qn()}},"settingsData"),P=l((e,t)=>{e.writeHead(303,{location:t,"cache-control":"no-store"}),e.end()},"redirect");async function Ya(e,t,n){const r=l((s,o,u,f={})=>{t.writeHead(s,{"content-type":o,"cache-control":"no-store",...f}),t.end(u)},"send"),a=l((s,o)=>{const u=xn();return r(s,"application/json",JSON.stringify(o),u?{"x-hub-code":u}:{})},"sendJSON"),d=l(({route:s,started:o})=>(o.then(({error:u})=>{u&&c(`new board /${s} didn't start: ${u}`),_.refresh()}),_.refresh(),a(200,{route:s})),"starting"),h=Qa(e),i=n.pathname,S=/^\/_hub\/close-board\/([a-z0-9-]+)$/.exec(i);if(S&&e.method==="GET")try{return a(200,await Xn(S[1]))}catch(s){return a(s.status||400,{error:s.message})}if(["/_hub/close-board","/_hub/reopen-board"].includes(i)&&e.method==="POST"){let s;try{const o=await x(e);if(s=o.route,i==="/_hub/reopen-board"){const T=await ea(s);return _.refresh(),a(200,T)}if(A.has(s))return a(409,{error:"That board is already closing."});A.add(s);const u=l(()=>A.delete(s),"release");t.once("finish",u),t.once("close",u),await D.running;const f=await Yn(s,o.name);return _.refresh(),a(200,f)}catch(o){return a(o.status||400,{error:o.message})}}if(i==="/_hub/restart-board"&&e.method==="POST"){let s;try{if(s=(await x(e)).route,U.has(s))return a(409,{error:"That board is already restarting."});U.add(s),await D.running;const o=await ta(s,{serverUp:Ma,portUp:ke,onStarting:l(u=>c(`restart /${s}: ${u}\u2019s session is back, its server is still starting`),"onStarting")});return c(`restarted /${s}: server answers, session ${o.session} resumed`),a(200,o)}catch(o){return s&&c(`restart /${s} failed: ${o.message}`),a(o.status||400,{error:o.message})}finally{U.delete(s),_.refresh()}}if(i==="/_hub/restart-hub"&&e.method==="POST")try{return await x(e),$e?a(409,{error:"The hub is already restarting."}):($e=!0,t.once("finish",()=>{c("restarting the hub from the hub page");const s=Pa("hub");s&&($e=!1,c(`hub restart failed: ${s}`))}),a(200,{restarting:!0,command:Ra().restartCommand}))}catch(s){return a(s.status||400,{error:s.message})}if(i==="/_hub/upgrade"&&e.method==="POST")try{return await x(e),a(200,{upgrade:await W.request()})}catch(s){return a(s.status||400,{error:s.message})}if(i==="/_hub/presence"&&e.method==="POST")return await x(e),ht(H.beat()),t.writeHead(204,{"cache-control":"no-store"}),t.end();if(i==="/_hub/wake"&&e.method==="POST")try{const{route:s}=await x(e),o=R(g()).find(u=>u.route===s);return o?(xe.add(s),await D.running,pt(),await D.running,Oe.clear(),_.changed(s),a(200,{route:s,listen:Ge(o)})):a(404,{error:"That board is not open."})}catch(s){return a(s.status||400,{error:s.message})}if(i==="/_hub/repos"&&e.method==="GET")try{return a(200,await L.list(g(),{fresh:n.searchParams.get("fresh")==="1"}))}catch(s){return a(s.status||502,{error:s.message})}if(i==="/_hub/branches"&&e.method==="GET")try{return a(200,await L.branches(n.searchParams.get("github")??n.searchParams.get("remote")))}catch(s){return a(s.status||502,{error:s.message})}if(i==="/_hub/repo"&&e.method==="GET")return Nn(n,{repos:L,manifest:g(),sendJSON:a});const k=/^\/_hub\/board-repo\/([a-z0-9-]+)$/.exec(i);if(k&&["GET","POST"].includes(e.method))try{const s=e.method==="GET"?Ve(k[1]):await Gn(k[1],await x(e),{repos:L});return e.method==="POST"&&_.refresh(),a(200,s)}catch(s){return a(s.status||400,{error:s.message})}if(i==="/_hub/new-board"&&e.method==="POST"){if(!me(e.headers))return r(403,"text/plain; charset=utf-8","Send name, github, branch and purpose as application/json.");let s;try{s=await x(e)}catch{return r(400,"text/plain; charset=utf-8","Send name, github, branch and purpose as JSON.")}try{return d(await Jn(s,{repos:L,changed:l(()=>_.refresh(),"changed")}))}catch(o){if(o.status)return r(o.status,"text/plain; charset=utf-8",o.message);throw o}}const y=/^\/_hub\/new-board\/([a-z0-9-]+)\/retry$/.exec(i);if(y&&e.method==="POST"){if(!me(e.headers))return r(403,"text/plain; charset=utf-8","Send the retry as application/json.");try{return d(await Kn(y[1],{repos:L,changed:l(()=>_.refresh(),"changed")}))}catch(s){if(s.status)return r(s.status,"text/plain; charset=utf-8",s.message);throw s}}const m=/^\/_hub\/new-board\/([a-z0-9-]+)$/.exec(i);if(m&&e.method==="DELETE")try{const s=await Vn(m[1]);return _.refresh(),a(200,s)}catch(s){if(s.status)return r(s.status,"text/plain; charset=utf-8",s.message);throw s}if((i==="/_hub/build"||i==="/_hub/pull")&&e.method==="POST"){if(ne)return a(503,{error:"The hub is stopping. Try again after it restarts."});const s=["127.0.0.1","::1","::ffff:127.0.0.1"],o=(e.headers.host||"").replace(/:\d+$/,"");if(!s.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:`${i==="/_hub/pull"?"Pulls":"Builds"} can only be started from the host over loopback.`});let u;try{u=await x(e)}catch(f){return a(f.status||400,{error:i==="/_hub/pull"?"Send the board route as JSON.":"Send the board route and card key as JSON."})}try{return i==="/_hub/pull"?a(200,await ya(u,{manifest:g()})):a(202,await ga(u,{manifest:g(),log:c}))}catch(f){if(f instanceof ma)return a(f.status,{error:f.message});throw f}}if(i==="/sw.js")return r(200,"text/javascript; charset=utf-8",w.readFileSync($.join(X,"sw.js")),{"service-worker-allowed":"/"});if(i==="/manifest.webmanifest")return r(200,"application/manifest+json",Za());const p=/^\/_hub\/icon-(180|192|512)\.png$/.exec(i);if(p)try{return r(200,"image/png",w.readFileSync($.join(X,`icon-${p[1]}.png`)),{"cache-control":"max-age=86400"})}catch{t.writeHead(404).end();return}if(i==="/_hub/colors.css"){const s=n.searchParams.get("v")===Ze()?"public, max-age=31536000, immutable":"no-cache";try{return r(200,"text/css; charset=utf-8",w.readFileSync(La),{"cache-control":s})}catch{t.writeHead(404).end();return}}if(i==="/_hub/kickflip.png")try{return r(200,"image/png",w.readFileSync($.join(ge,"kickflip.png")),{"cache-control":"max-age=86400"})}catch{t.writeHead(404).end();return}if(i==="/_hub/hub.css")return r(200,"text/css; charset=utf-8",w.readFileSync($.join(ge,"hub.css")));if(i==="/_hub/sidebar.json")return a(200,(await ue({fresh:n.searchParams.get("fresh")==="1"})).data);if(i==="/_hub/stream"&&e.method==="GET"){_.handle(e,t);return}if(i.startsWith("/_hub/app/")){const s=sa(i.slice(10));if(!s){t.writeHead(404).end();return}const o=$.extname(s),u={".js":"text/javascript; charset=utf-8",".css":"text/css; charset=utf-8",".png":"image/png"}[o],f=n.searchParams.get("v")===oa(s)?"public, max-age=31536000, immutable":"no-cache";return o===".png"?r(200,u,w.readFileSync(s),{"cache-control":f}):vt(e,t,200,{"content-type":u,"cache-control":f},w.readFileSync(s))}if(i==="/_hub/api/hub")return a(200,await de(e));if(i==="/_hub/api/settings")return a(200,await J());if(i==="/_hub/push/key")return a(200,{key:At()});if(i==="/_hub/push/subscribe"&&e.method==="POST"){const s=e.headers["x-forwarded-host"]||e.headers.host;return a(200,{count:Ht(await x(e),`https://${s}/`,e.headers["user-agent"])})}if(i==="/_hub/push/unsubscribe"&&e.method==="POST")return a(200,{count:It((await x(e))?.endpoint)});if(i==="/_hub/push/test"&&e.method==="POST"){const s=await Mt({title:"Claude",body:"Test notification from the hub.",url:"/",tag:"hub-test"});return a(200,{sent:s.filter(o=>o.status>=200&&o.status<300).length,results:s})}if(i==="/_hub/settings/notifications"&&e.method==="POST"){const s=new URLSearchParams(await E(e)),o=s.get("set"),u=o==="on"?{on:s.get("value")==="on"}:o==="weekends"?{weekends_off:s.get("value")==="on"}:s.has("snooze")?{snooze:s.get("snooze")}:null,f="Pass set=on or set=weekends with value=on, or snooze.";if(!u)return h?a(400,{error:f}):r(400,"text/plain; charset=utf-8",f);let T;try{T=await Ut(u)}catch(K){if(K.status!==400)throw K;return h?a(400,{error:hn}):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 J()):P(t,"/_hub/#notifications")}if(i==="/_hub/settings/keep-awake"&&e.method==="POST"){const s=new URLSearchParams(await E(e));try{mn(s.get("keep_awake"))}catch(o){if(o.status===409)return h?a(409,{error:o.message}):r(409,"text/plain; charset=utf-8",o.message);if(o.status!==400)throw o;return h?a(400,{error:fn}):P(t,"/_hub/?refused=keep-awake#keep-awake")}return ee.apply(),h?a(200,await J()):P(t,"/_hub/#keep-awake")}if(i==="/_hub/settings/codex"&&e.method==="POST"){const s=new URLSearchParams(await E(e));try{Nt(s.get("value"),Z({fresh:!0}))}catch(o){if(o.status!==400&&o.status!==409)throw o;return h?a(o.status,{error:o.message}):r(o.status,"text/plain; charset=utf-8",o.message)}return h?a(200,await de(e)):P(t,"/_hub/")}if(i==="/_hub/settings/claude-model"&&e.method==="POST"){const s=new URLSearchParams(await E(e));try{zt(s.get("value"))}catch(o){if(o.status!==400)throw o;return h?a(400,{error:o.message}):r(400,"text/plain; charset=utf-8",o.message)}return h?a(200,await de(e)):P(t,"/_hub/")}if(["/_hub/settings/pause-at","/_hub/settings/keep-going"].includes(i)&&e.method==="POST"){const s=new URLSearchParams(await E(e));try{i.endsWith("/pause-at")?ha(s.get("value")):fa(s.get("value")),await Te()}catch(o){if(o.status!==400&&o.status!==409)throw o;return h?a(o.status,{error:o.message}):r(o.status,"text/plain; charset=utf-8",o.message)}return h?a(200,await de(e)):P(t,"/_hub/")}if(i==="/_hub/settings/linear"&&e.method==="POST"){const s=new URLSearchParams(await E(e,4096));try{s.has("remove")?(Ea(),c("linear: key removed"),await ut()):c(`linear: key ${(await Da(s.get("key"))).refused?"refused by Linear":"saved"}`)}catch(o){if(!(o instanceof be))throw o;return h?a(o.status,{error:o.message}):r(o.status,"text/plain; charset=utf-8",o.message)}return h?a(200,await J()):P(t,"/_hub/#linear")}if(i==="/_hub/api/linear"&&e.method==="GET"){const s=Q();if(!s.set||s.refused)return a(200,s);const o=nt();try{const u=await o.teams();return a(200,{...Q(),teams:u.map(({key:f,name:T})=>({key:f,name:T})),projects:u.flatMap(f=>f.projects.map(T=>({id:T.id,name:T.name,team:f.key})))})}catch(u){if(!(u instanceof be))throw u;return a(200,{...Q(),error:u.message})}}if(i==="/_hub/linear/projects"&&e.method==="POST"){const s=nt();if(!s)return a(400,{error:"Add a Linear key on Hub first."});let o;try{o=await x(e,4*1024)}catch(f){return a(f.status||400,{error:"Send the team and project name as JSON."})}const u=typeof o.name=="string"?o.name.trim():"";if(!u||u.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 s.createProject(o.team,u))}catch(f){if(!(f instanceof be))throw f;return a(f.status,{error:f.message})}}if(i==="/_hub/settings/quiet"&&e.method==="POST"){const s=new URLSearchParams(await E(e)),o=Object.fromEntries(["start","end"].filter(u=>s.has(u)).map(u=>[u,s.get(u).trim()]));s.get("set")==="hold"&&(o.hold=s.get("value")||"off");try{Bt(sn(V(),o))}catch(u){if(u.status!==400)throw u;return h?a(400,{error:u.message}):r(400,"text/plain; charset=utf-8",u.message)}return _.refresh(),h?a(200,await J()):P(t,"/_hub/#quiet")}if(i==="/_hub/settings/rules"&&e.method==="POST"){let s,o;try{s=await x(e,64*1024),o=En(pe(),s.rules,s.base??[]),Ke(o.changes)&&Dn(o.rules)}catch(f){if(!(f instanceof Rn)&&!(f instanceof SyntaxError))throw f;return a(400,{error:f.message})}if(!Ke(o.changes))return a(200,{rules:o.rules,changed:!1,events:[],failed:[]});const u=await ze(R(g()),"rules",{scope:"hub",...o.changes});return c(`hub rules saved: rules \u2192 ${u.events.map(f=>f.route).join(", ")||"no board"}${u.failed.length?`; ${u.failed.join("; ")}`:""}`),a(200,{rules:o.rules,changed:!0,...u})}if(i==="/_hub/push/flush"&&e.method==="POST"){const s=await Ce();return s&&c(`push queue: ${s.ok?"sent":"could not send"} ${s.count} queued (from the settings page)`),h?a(200,dn(s)):P(t,"/_hub/")}if(i==="/_hub/poll"&&e.method==="POST"){const s=await x(e);return a(200,await _e({flush:s.flush!==!1}))}if(i==="/_hub/prompts"&&e.method==="POST"){let s;try{s=await x(e,8*1024*1024)}catch(f){if(f.status)throw f;return a(400,{error:"Send the hook input as JSON."})}if(!s?.tool_name||s.hook_event_name&&s.hook_event_name!=="PermissionRequest")return a(400,{error:"Send a PermissionRequest hook input."});const{answer:o,rec:u}=await M(f=>{const T=ve(),K=T.boards.find(Pe=>Pe.owner?.session&&Pe.owner.session===s.session_id)||null;fe(f,T)&&_.refresh();const je=en(f,s);if(je)return{answer:je};const he=Qt(s,{board:K,live:T.running?.get(s.session_id)});return f.prompts[he.id]=he,{rec:he}});return o?a(200,{state:"answered",answer:o}):(G(u.route),await gt(s),c(`prompt ${u.id}: ${u.route?`/${u.route}`:u.cwd} ${u.tool}${u.action?` (${u.action})`:""}`),Wt(Xt(u),{kind:"prompt",immediate:!0}).catch(f=>c(`prompt ${u.id}: push failed: ${f.message}`)),a(200,{id:u.id,deadline:u.deadline}))}if(i==="/_hub/permission-denied"&&e.method==="POST"){let s;try{s=await x(e,8*1024*1024)}catch(o){if(o.status)throw o;return a(400,{error:"Send the hook input as JSON."})}return s?.hook_event_name!=="PermissionDenied"?a(400,{error:"Send a PermissionDenied hook input."}):a(200,{noted:await gt(s)})}const v=/^\/_hub\/prompts\/([\w-]+)(\/wait|\/answer)?$/.exec(i);return v?qa(e,t,{id:v[1],op:v[2]?.slice(1)||null,url:n,sendJSON:a}):!1}l(Ya,"hubRoute");async function qa(e,t,{id:n,op:r,url:a,sendJSON:d}){if(r==="wait"&&e.method==="GET"){const h=l(()=>M(m=>{const p=j(m.prompts,n);return p?.answer?delete m.prompts[n]:p&&(p.seen=new Date().toISOString()),p}),"seen"),i=l(m=>(G(m.route),d(200,{state:"answered",answer:m.answer})),"take"),S=await h();if(!S)return d(404,{state:"gone"});if(S.answer)return i(S);const k=Math.max(0,Math.min(Number(a.searchParams.get("ms"))||Ie,Ie));if(await new Promise(m=>{const p=N.get(n)||new Set;N.set(n,p);const v=l(()=>{clearTimeout(s),p.delete(v),!p.size&&N.get(n)===p&&N.delete(n),m()},"done"),s=setTimeout(v,k);p.add(v),t.on("close",v)}),t.writableEnded||t.destroyed)return;const y=await h();return y?y.answer?i(y):d(200,{state:"pending"}):d(404,{state:"gone"})}if(r==="answer"&&e.method==="POST"){const h=await x(e),i=await M(y=>{const m=j(y.prompts,n);if(!m)return{status:404,error:"That prompt has expired."};if(m.answer)return{status:409,error:"That prompt was already answered."};const{answer:p,error:v}=Vt(m,h);if(v)return{status:400,error:v};m.answer=p;const s=ve();return fe(y,s),qt(y,m,s),{rec:m,answer:p}});if(i.error)return d(i.status,{error:i.error});const{rec:S,answer:k}=i;c(`prompt ${n}: ${k.choice}`);for(const y of[...N.get(n)||[]])y();return G(S.route),d(200,{ok:!0})}if(r===null&&e.method==="DELETE"){const h=await M(i=>{const S=j(i.prompts,n);return delete i.prompts[n],S});return h&&(G(h.route),c(`prompt ${n}: its hook gave up`)),d(200,{ok:!0})}return!1}l(qa,"promptRoute");const es=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 Ot(e,t){const n=O.get(e)||{},r=n.exitCode!==void 0&&n.exitCode!==null?` (last exit: ${n.exitCode})`:"",a=B(t),[d,h]=a==="port"?["down",`down \xB7 nothing answers on port ${t.port}`]:a==="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${r}`],i=a==="cmd"?"The hub runs every site and restarts one that stops.":a==="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`${Tt(`${t.title||e} is down`,'<meta http-equiv="refresh" content="5">')}
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">')}
10
10
  <a class="back" href="/_hub/">\u2039 Hub</a>
11
- <h1>${I(t.title||e)} isn't answering</h1>
12
- <p class="lede">${I(i)}</p>
11
+ <h1>${M(t.title||e)} isn't answering</h1>
12
+ <p class="lede">${M(h)}</p>
13
13
  <h2 class="label">Site</h2>
14
- <div class="group">${es({icon:Ln(e,t),title:I(t.title||e),sub:`<span class="st ${d}">${I(h)}</span>`})}</div>
14
+ <div class="group">${$s({icon:Gn(e,t),title:M(t.title||e),sub:`<span class="st ${r}">${M(p)}</span>`})}</div>
15
15
  <p class="note">This page retries every 5 seconds.</p>
16
- </main></body></html>`}l(Ot,"downPage");const jt=l(e=>new URL(e.url.replace(/^\/+/,"/"),"http://hub"),"requestURL"),Pt=Re.createServer((e,t)=>{et(e).then(n=>n==="owner"?ts(e,t):Oa(e,t,n))});function ts(e,t){const n=jt(e);if(e.method!=="GET"&&e.method!=="HEAD"&&aa(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 r=new Set(Object.keys(g().sites));if(Qe({method:e.method,pathname:n.pathname,search:n.search,dest:ra(e.headers),sites:r})==="shell"){const m=n.pathname==="/"?"":n.pathname.startsWith("/_hub/")?n.pathname.slice(1):decodeURIComponent(n.pathname.split("/")[1]);Ka(m).then(p=>vt(e,t,200,{"content-type":"text/html; charset=utf-8","cache-control":"no-cache",vary:"sec-fetch-dest","x-app-shell":"1"},p)).catch(p=>{t.headersSent||t.writeHead(500,{"content-type":"text/plain"}),t.end(String(p.message||p))});return}if(n.pathname==="/_hub"){t.writeHead(302,{location:"/_hub/"+n.search}).end();return}if(n.pathname==="/_hub/status.json"){t.writeHead(200,{"content-type":"application/json","cache-control":"no-store"}),t.end(JSON.stringify({hub:{port:ye,pid:process.pid},sites:ie()},null,2));return}if(n.pathname==="/_hub/needs.json"){const m=ie();_t(m,{fresh:n.searchParams.get("fresh")==="1"}).then(p=>{t.writeHead(200,{"content-type":"application/json","cache-control":"no-store"}),t.end(JSON.stringify({total:xt(p),sites:Object.fromEntries(m.map(v=>[v.route,p.get(v.route)??null]))}))}).catch(p=>{t.headersSent||t.writeHead(500,{"content-type":"text/plain"}),t.end(String(p.message||p))});return}if(n.pathname==="/favicon.ico"){t.writeHead(301,{location:"/_hub/kickflip.png"}).end();return}if(n.pathname==="/sw.js"||n.pathname==="/manifest.webmanifest"||n.pathname.startsWith("/_hub/")){Ya(e,t,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=n.pathname.split("/")[1],d=j(g().sites,a);if(!d){t.writeHead(404,{"content-type":"text/html; charset=utf-8"}),t.end(`${Tt("No Site")}<a class="back" href="/_hub/">\u2039 Hub</a><h1>No site at /${I(a)}</h1></main></body></html>`);return}if(n.pathname==="/"+a){t.writeHead(302,{location:"/"+a+"/"+n.search}).end();return}const h="/"+a,i=e.url.slice(h.length)||"/",S=Qe({pathname:n.pathname,dest:"document",sites:r})==="shell";if(B(d)==="files"){if(!kt(d.dir)){t.writeHead(502,{"content-type":"text/html; charset=utf-8","cache-control":"no-store"}),t.end(Ot(a,d));return}ka(e,t,{dir:d.dir,rest:i,prefix:h,headers:S?{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"},y=Re.request({host:"127.0.0.1",port:d.port,method:e.method,path:i,headers:k},m=>{const p={...m.headers};S&&(p.vary=[p.vary,"sec-fetch-dest"].filter(Boolean).join(", ")),typeof p.location=="string"&&p.location.startsWith("/")&&!p.location.startsWith("//")&&!p.location.startsWith(h+"/")&&p.location!==h&&(p.location=h+p.location),t.writeHead(m.statusCode,p),m.pipe(t),m.on("close",()=>{m.complete||t.destroy()})});t.on("close",()=>{t.writableFinished||y.destroy()}),y.on("error",()=>{if(t.headersSent){t.destroy();return}t.writeHead(502,{"content-type":"text/html; charset=utf-8","cache-control":"no-store"}),t.end(Ot(a,d))}),e.pipe(y)}l(ts,"serve"),Pt.on("upgrade",async(e,t,n)=>{const r=await et(e);if(r!=="owner"){ja(t,r);return}const a=jt(e).pathname.split("/")[1],d=j(g().sites,a);if(!d?.port){t.destroy();return}const h=`/${a}`;Ta({req:e,socket:t,head:n,port:d.port,path:e.url.slice(h.length)||"/",replace:["x-forwarded-prefix"],extra:[`X-Forwarded-Prefix: ${h}`]})}),Pt.listen(ye,"127.0.0.1",()=>c(`hub on http://127.0.0.1:${ye}/ (manifest ${at})`));
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})`));
package/hub/linear.mjs CHANGED
@@ -1,2 +1 @@
1
- var j=Object.defineProperty;var a=(s,e)=>j(s,"name",{value:e,configurable:!0});import f from"node:fs";import v from"node:path";import _ from"node:os";class p extends Error{static{a(this,"LinearError")}constructor(e,r=502){super(e),this.status=r}}const k=a((s=_.homedir())=>v.join(s,".claude","kickflip","linear.json"),"keyFile"),C=a(()=>process.env.KICKFLIP_LINEAR_URL||"https://api.linear.app/graphql","endpoint");function g(s){try{const e=JSON.parse(f.readFileSync(k(s),"utf8"));return typeof e?.key=="string"&&e.key?e:null}catch{return null}}a(g,"readLinearKey");function $(s){const e=g(s);return e?{set:!0,...e.refused_at?{refused:!0}:{},...e.workspace?{workspace:e.workspace,url_key:e.url_key}:{}}:{set:!1}}a($,"linearStatus");const E=a(s=>{const e=$(s);return e.set&&!e.refused},"linearWorks"),P="Linear needs Hub\u2019s Linear key; add it in Hub settings \u203A Linear first, or pick Internal or GitHub issues.";function I(s){const e=k();f.mkdirSync(v.dirname(e),{recursive:!0});const r=`${e}.${process.pid}.tmp`;f.writeFileSync(r,JSON.stringify(s,null,2)+`
2
- `,{mode:384}),f.chmodSync(r,384),f.renameSync(r,e)}a(I,"writeKeyFile");function L(s,{fetch:e=globalThis.fetch,checked:r=a(()=>{},"checked")}={}){async function u(n,t={}){let i;try{i=await e(C(),{method:"POST",headers:{"content-type":"application/json",authorization:s},body:JSON.stringify({query:n,variables:t}),signal:AbortSignal.timeout(15e3)})}catch(c){throw new p(`Could not reach Linear: ${c.message}`)}const o=await i.json().catch(()=>null);if(i.status===401||o?.errors?.some(c=>c.extensions?.type==="authentication error"))throw r(!1),new p("Linear refused the key.",400);if(o?.errors?.length){const c=o.errors[0];throw Object.assign(new p(`Linear: ${c.message}`),{notFound:c.extensions?.type==="not found"||/not found/i.test(c.message)})}if(!i.ok||!o?.data)throw new p(`Linear answered ${i.status}.`);return r(!0),o.data}a(u,"call");const l="id key name states { nodes { id name type position } } projects(first: 100) { nodes { id name } }";async function d(n){return(await u(`query($key: String!) { teams(filter: { key: { eq: $key } }) { nodes { ${l} } } }`,{key:n})).teams.nodes[0]||(()=>{throw new p(`Linear has no team ${n}.`,400)})()}a(d,"team");const m=a((n,t)=>n.projects.nodes.find(i=>i.id===t)||n.projects.nodes.find(i=>i.name.toLowerCase()===String(t).toLowerCase())||(()=>{throw new p(`Linear's ${n.key} team has no project ${t}.`,400)})(),"projectOf"),w={async viewer(){const n=await u("query { viewer { id name } organization { name urlKey } }");return{viewer:{id:n.viewer.id,name:n.viewer.name},workspace:n.organization.name,url_key:n.organization.urlKey}},async teams(){const n=await u("query { teams(first: 100) { nodes { id key name } } }");return Promise.all(n.teams.nodes.map(async t=>{const i=await u("query($id: String!) { team(id: $id) { projects(first: 100) { nodes { id name } } } }",{id:t.id});return{id:t.id,key:t.key,name:t.name,projects:i.team.projects.nodes.map(o=>({id:o.id,name:o.name}))}}))},async createProject(n,t){const i=await d(n),o=await u("mutation($input: ProjectCreateInput!) { projectCreate(input: $input) { success project { id name } } }",{input:{name:t,teamIds:[i.id]}});if(!o.projectCreate.success)throw new p("Linear did not create the project.");return{...o.projectCreate.project,team:i.key}},async createIssue({team:n,project:t,title:i,description:o}){const c=await d(n),y={teamId:c.id,projectId:m(c,t).id,title:i,...o?{description:o}:{}},h=await u("mutation($input: IssueCreateInput!) { issueCreate(input: $input) { success issue { identifier url } } }",{input:y});if(!h.issueCreate.success)throw new p("Linear did not create the issue.");return h.issueCreate.issue},async issue(n){try{const t=await u("query($id: String!) { issue(id: $id) { identifier team { key } assignee { id } state { id name type } } }",{id:n});return t.issue?{identifier:t.issue.identifier,team:t.issue.team.key,assignee:t.issue.assignee?.id??null,state:t.issue.state}:null}catch(t){if(t.notFound)return null;throw t}},async advanceIssue(n,t,{owner:i,created:o}={}){const c=await w.issue(n);if(!c||c.assignee!==i&&!o||x(c.state)>={started:1,review:2,done:3}[t])return null;const y=S((await d(c.team)).states.nodes,t);if(!y)return null;if(!(await u("mutation($id: String!, $stateId: String!) { issueUpdate(id: $id, input: { stateId: $stateId }) { success } }",{id:n,stateId:y.id})).issueUpdate.success)throw new p("Linear did not move the issue.");return y.name},async moveIssue(n,t){const i=await w.issue(n);if(!i)throw new p(`Linear has no issue ${n}.`,400);if(["completed","canceled"].includes(i.state.type))return null;const o=S((await d(i.team)).states.nodes,t);if(!o||o.id===i.state.id)return null;if(!(await u("mutation($id: String!, $stateId: String!) { issueUpdate(id: $id, input: { stateId: $stateId }) { success } }",{id:n,stateId:o.id})).issueUpdate.success)throw new p("Linear did not move the issue.");return o.name}};return w}a(L,"linearClient");function x(s){return["completed","canceled"].includes(s.type)?3:s.type==="started"?/review/i.test(s.name)?2:1:0}a(x,"stageOf");function S(s,e){const r=[...s].sort((d,m)=>d.position-m.position),u=a(d=>r.filter(m=>m.type===d),"of"),l=a(d=>/review/i.test(d.name),"review");return e==="started"?u("started").find(d=>/^in progress$/i.test(d.name))||u("started").find(d=>!l(d))||null:e==="review"?u("started").find(l)||null:e==="done"&&(u("completed").find(d=>/^done$/i.test(d.name))||u("completed")[0])||null}a(S,"stateFor");async function N(s,{fetch:e}={}){const r=typeof s=="string"?s.trim():"";if(!r||/\s/.test(r)||r.length>200)throw new p("Paste a Linear personal API key.",400);const u=new Date().toISOString();let l=!1;const d=await L(r,{fetch:e,checked:a(m=>{l=!m},"checked")}).viewer().catch(m=>{if(!l)throw m;return null});return I(d?{key:r,...d,saved_at:u}:{key:r,saved_at:u,refused_at:u}),$()}a(N,"saveLinearKey");function F(s,e){const r=g();if(r?.key!==s||!!r.refused_at==!e)return;const{refused_at:u,...l}=r;I(e?l:{...l,refused_at:new Date().toISOString()})}a(F,"markChecked");function U(){return f.rmSync(k(),{force:!0}),$()}a(U,"removeLinearKey");function z({fetch:s}={}){const e=g();return e?{...L(e.key,{fetch:s,checked:a(r=>F(e.key,r),"checked")}),saved:e}:null}a(z,"savedClient");export{P as LINEAR_NEEDS_KEY,p as LinearError,L as linearClient,$ as linearStatus,E as linearWorks,g as readLinearKey,U as removeLinearKey,N as saveLinearKey,z as savedClient,x as stageOf,S as stateFor};
1
+ var I=Object.defineProperty;var a=(s,e)=>I(s,"name",{value:e,configurable:!0});import{readSecret as L,writeSecret as h}from"./secrets.mjs";class l extends Error{static{a(this,"LinearError")}constructor(e,u=502){super(e),this.status=u}}const S=a(()=>process.env.KICKFLIP_LINEAR_URL||"https://api.linear.app/graphql","endpoint");function k(s){const e=L("linear",s);return typeof e?.key=="string"&&e.key?e:null}a(k,"readLinearKey");function g(s){const e=k(s);return e?{set:!0,...e.refused_at?{refused:!0}:{},...e.workspace?{workspace:e.workspace,url_key:e.url_key}:{}}:{set:!1}}a(g,"linearStatus");const K=a(s=>{const e=g(s);return e.set&&!e.refused},"linearWorks"),b="Linear needs Hub\u2019s Linear key; add it in Hub settings \u203A Linear first, or pick Internal or GitHub issues.";function $(s,{fetch:e=globalThis.fetch,checked:u=a(()=>{},"checked")}={}){async function o(n,t={}){let r;try{r=await e(S(),{method:"POST",headers:{"content-type":"application/json",authorization:s},body:JSON.stringify({query:n,variables:t}),signal:AbortSignal.timeout(15e3)})}catch(c){throw new l(`Could not reach Linear: ${c.message}`)}const i=await r.json().catch(()=>null);if(r.status===401||i?.errors?.some(c=>c.extensions?.type==="authentication error"))throw u(!1),new l("Linear refused the key.",400);if(i?.errors?.length){const c=i.errors[0];throw Object.assign(new l(`Linear: ${c.message}`),{notFound:c.extensions?.type==="not found"||/not found/i.test(c.message)})}if(!r.ok||!i?.data)throw new l(`Linear answered ${r.status}.`);return u(!0),i.data}a(o,"call");const p="id key name states { nodes { id name type position } } projects(first: 100) { nodes { id name } }";async function d(n){return(await o(`query($key: String!) { teams(filter: { key: { eq: $key } }) { nodes { ${p} } } }`,{key:n})).teams.nodes[0]||(()=>{throw new l(`Linear has no team ${n}.`,400)})()}a(d,"team");const f=a((n,t)=>n.projects.nodes.find(r=>r.id===t)||n.projects.nodes.find(r=>r.name.toLowerCase()===String(t).toLowerCase())||(()=>{throw new l(`Linear's ${n.key} team has no project ${t}.`,400)})(),"projectOf"),w={async viewer(){const n=await o("query { viewer { id name } organization { name urlKey } }");return{viewer:{id:n.viewer.id,name:n.viewer.name},workspace:n.organization.name,url_key:n.organization.urlKey}},async teams(){const n=await o("query { teams(first: 100) { nodes { id key name } } }");return Promise.all(n.teams.nodes.map(async t=>{const r=await o("query($id: String!) { team(id: $id) { projects(first: 100) { nodes { id name } } } }",{id:t.id});return{id:t.id,key:t.key,name:t.name,projects:r.team.projects.nodes.map(i=>({id:i.id,name:i.name}))}}))},async createProject(n,t){const r=await d(n),i=await o("mutation($input: ProjectCreateInput!) { projectCreate(input: $input) { success project { id name } } }",{input:{name:t,teamIds:[r.id]}});if(!i.projectCreate.success)throw new l("Linear did not create the project.");return{...i.projectCreate.project,team:r.key}},async createIssue({team:n,project:t,title:r,description:i}){const c=await d(n),m={teamId:c.id,projectId:f(c,t).id,title:r,...i?{description:i}:{}},y=await o("mutation($input: IssueCreateInput!) { issueCreate(input: $input) { success issue { identifier url } } }",{input:m});if(!y.issueCreate.success)throw new l("Linear did not create the issue.");return y.issueCreate.issue},async issue(n){try{const t=await o("query($id: String!) { issue(id: $id) { identifier team { key } assignee { id } state { id name type } } }",{id:n});return t.issue?{identifier:t.issue.identifier,team:t.issue.team.key,assignee:t.issue.assignee?.id??null,state:t.issue.state}:null}catch(t){if(t.notFound)return null;throw t}},async advanceIssue(n,t,{owner:r,created:i}={}){const c=await w.issue(n);if(!c||c.assignee!==r&&!i||j(c.state)>={started:1,review:2,done:3}[t])return null;const m=v((await d(c.team)).states.nodes,t);if(!m)return null;if(!(await o("mutation($id: String!, $stateId: String!) { issueUpdate(id: $id, input: { stateId: $stateId }) { success } }",{id:n,stateId:m.id})).issueUpdate.success)throw new l("Linear did not move the issue.");return m.name},async moveIssue(n,t){const r=await w.issue(n);if(!r)throw new l(`Linear has no issue ${n}.`,400);if(["completed","canceled"].includes(r.state.type))return null;const i=v((await d(r.team)).states.nodes,t);if(!i||i.id===r.state.id)return null;if(!(await o("mutation($id: String!, $stateId: String!) { issueUpdate(id: $id, input: { stateId: $stateId }) { success } }",{id:n,stateId:i.id})).issueUpdate.success)throw new l("Linear did not move the issue.");return i.name}};return w}a($,"linearClient");function j(s){return["completed","canceled"].includes(s.type)?3:s.type==="started"?/review/i.test(s.name)?2:1:0}a(j,"stageOf");function v(s,e){const u=[...s].sort((d,f)=>d.position-f.position),o=a(d=>u.filter(f=>f.type===d),"of"),p=a(d=>/review/i.test(d.name),"review");return e==="started"?o("started").find(d=>/^in progress$/i.test(d.name))||o("started").find(d=>!p(d))||null:e==="review"?o("started").find(p)||null:e==="done"&&(o("completed").find(d=>/^done$/i.test(d.name))||o("completed")[0])||null}a(v,"stateFor");async function q(s,{fetch:e}={}){const u=typeof s=="string"?s.trim():"";if(!u||/\s/.test(u)||u.length>200)throw new l("Paste a Linear personal API key.",400);const o=new Date().toISOString();let p=!1;const d=await $(u,{fetch:e,checked:a(f=>{p=!f},"checked")}).viewer().catch(f=>{if(!p)throw f;return null});return h("linear",d?{key:u,...d,saved_at:o}:{key:u,saved_at:o,refused_at:o}),g()}a(q,"saveLinearKey");function _(s,e){const u=k();if(u?.key!==s||!!u.refused_at==!e)return;const{refused_at:o,...p}=u;h("linear",e?p:{...p,refused_at:new Date().toISOString()})}a(_,"markChecked");function E(){return h("linear",null),g()}a(E,"removeLinearKey");function O({fetch:s}={}){const e=k();return e?{...$(e.key,{fetch:s,checked:a(u=>_(e.key,u),"checked")}),saved:e}:null}a(O,"savedClient");export{b as LINEAR_NEEDS_KEY,l as LinearError,$ as linearClient,g as linearStatus,K as linearWorks,k as readLinearKey,E as removeLinearKey,q as saveLinearKey,O as savedClient,j as stageOf,v as stateFor};
package/hub/live.mjs CHANGED
@@ -1,2 +1,2 @@
1
- var S=Object.defineProperty;var s=(t,e)=>S(t,"name",{value:e,configurable:!0});import c from"node:fs";import f from"node:path";import g from"node:os";import{execFileSync as h}from"node:child_process";const l=f.join(g.homedir(),".claude","sessions"),a=2e3,u=s(t=>Date.parse(`${String(t).trim().replace(/\s+/g," ")} UTC`),"utc");function x(t,e){return t.filter(n=>{if(!e.has(n.pid))return!1;if(typeof n.procStart!="string")return!0;const r=u(n.procStart);return Number.isNaN(r)||Math.abs(r-e.get(n.pid))<=a})}s(x,"liveOf");function y(t=l){let e=[];try{e=c.readdirSync(t).filter(n=>n.endsWith(".json"))}catch{}return e.flatMap(n=>{try{const r=JSON.parse(c.readFileSync(f.join(t,n),"utf8"));return Number.isInteger(r?.pid)&&typeof r.sessionId=="string"?[r]:[]}catch{return[]}})}s(y,"sessionRecords");const N=s(t=>{try{return process.kill(t,0),!0}catch(e){return e.code==="EPERM"}},"running");function d(t){try{return h("ps",t,{encoding:"utf8",timeout:3e3,env:{...process.env,TZ:"UTC"}})}catch(e){return typeof e.status=="number"&&!e.signal?e.stdout||"":null}}s(d,"ps");function m(t){const e=new Map,n=t.filter(N);if(!n.length)return e;const r=d(["-o","pid=,lstart=","-p",n.join(",")]);if(r===null)return null;for(const o of r.split(`
2
- `)){const i=o.trim().match(/^(\d+)\s+(.+)$/);i&&e.set(Number(i[1]),u(i[2]))}return e}s(m,"processStarts");function O(t,e=m){if(!Number.isInteger(t?.pid)||t.pid<=0||!t.sessionId||typeof t.procStart!="string")return!1;const n=u(t.procStart);if(!Number.isFinite(n))return!1;const r=e([t.pid])?.get(t.pid);return Number.isFinite(r)&&Math.abs(n-r)<=a}s(O,"verifiedProcess");function b({dir:t=l,starts:e=m,session:n=null}={}){const r=y(t).filter(i=>!n||i.sessionId===n),o=e([...new Set(r.map(i=>i.pid))]);return o&&new Map(x(r,o).map(i=>[i.sessionId,i]))}s(b,"liveSessions");function I(t){const e=d(["-o","ppid=","-p",String(t)]);return e===null?void 0:Number(e.trim())||null}s(I,"parentOf");function R(t,e,{ppid:n=process.ppid,parent:r=I,live:o=b}={}){if(n!==e[0])return!0;const i=r(e[0]);if(i===null||i!==void 0&&e[1]!==void 0&&i!==e[1])return!0;const p=o({session:t});return p!==null&&!p.has(t)}s(R,"sessionGone");export{l as SESSIONS_DIR,x as liveOf,b as liveSessions,I as parentOf,m as processStarts,d as ps,R as sessionGone,y as sessionRecords,O as verifiedProcess};
1
+ var S=Object.defineProperty;var s=(t,e)=>S(t,"name",{value:e,configurable:!0});import c from"node:fs";import f from"node:path";import g from"node:os";import{execFileSync as h}from"node:child_process";const l=f.join(g.homedir(),".claude","sessions"),a=2e3,u=s(t=>Date.parse(`${String(t).trim().replace(/\s+/g," ")} UTC`),"utc");function x(t,e){return t.filter(n=>{if(!e.has(n.pid))return!1;if(typeof n.procStart!="string")return!0;const r=u(n.procStart);return Number.isNaN(r)||Math.abs(r-e.get(n.pid))<=a})}s(x,"liveOf");function N(t=l){let e=[];try{e=c.readdirSync(t).filter(n=>n.endsWith(".json"))}catch{}return e.flatMap(n=>{try{const r=JSON.parse(c.readFileSync(f.join(t,n),"utf8"));return Number.isInteger(r?.pid)&&typeof r.sessionId=="string"?[r]:[]}catch{return[]}})}s(N,"sessionRecords");const b=s(t=>{try{return process.kill(t,0),!0}catch(e){return e.code==="EPERM"}},"running");function d(t){const e=Number(process.env.KICKFLIP_PS_TIMEOUT_MS)||3e3;try{return h("ps",t,{encoding:"utf8",timeout:e,env:{...process.env,TZ:"UTC"}})}catch(n){return typeof n.status=="number"&&!n.signal?n.stdout||"":null}}s(d,"ps");function m(t){const e=new Map,n=t.filter(b);if(!n.length)return e;const r=d(["-o","pid=,lstart=","-p",n.join(",")]);if(r===null)return null;for(const o of r.split(`
2
+ `)){const i=o.trim().match(/^(\d+)\s+(.+)$/);i&&e.set(Number(i[1]),u(i[2]))}return e}s(m,"processStarts");function _(t,e=m){if(!Number.isInteger(t?.pid)||t.pid<=0||!t.sessionId||typeof t.procStart!="string")return!1;const n=u(t.procStart);if(!Number.isFinite(n))return!1;const r=e([t.pid])?.get(t.pid);return Number.isFinite(r)&&Math.abs(n-r)<=a}s(_,"verifiedProcess");function y({dir:t=l,starts:e=m,session:n=null}={}){const r=N(t).filter(i=>!n||i.sessionId===n),o=e([...new Set(r.map(i=>i.pid))]);return o&&new Map(x(r,o).map(i=>[i.sessionId,i]))}s(y,"liveSessions");function I(t){const e=d(["-o","ppid=","-p",String(t)]);return e===null?void 0:Number(e.trim())||null}s(I,"parentOf");function j(t,e,{ppid:n=process.ppid,parent:r=I,live:o=y}={}){if(n!==e[0])return!0;const i=r(e[0]);if(i===null||i!==void 0&&e[1]!==void 0&&i!==e[1])return!0;const p=o({session:t});return p!==null&&!p.has(t)}s(j,"sessionGone");export{l as SESSIONS_DIR,x as liveOf,y as liveSessions,I as parentOf,m as processStarts,d as ps,j as sessionGone,N as sessionRecords,_ as verifiedProcess};
package/hub/locks.mjs CHANGED
@@ -1 +1 @@
1
- var d=Object.defineProperty;var a=(r,c)=>d(r,"name",{value:c,configurable:!0});import t from"node:fs";import o from"node:path";async function u(r,c,{staleAfter:i=3e4,timeout:s=1e4,heartbeat:m=!1}={}){const e=`${r}.lock`;t.mkdirSync(o.dirname(r),{recursive:!0});const y=Date.now()+s;for(;;)try{t.mkdirSync(e);break}catch(n){if(n.code!=="EEXIST")throw n;try{if(Date.now()-t.statSync(e).mtimeMs>i){t.rmdirSync(e);continue}}catch{}if(Date.now()>y)throw Object.assign(new Error(`${o.basename(r)} is locked by another kickflip; try again`),{code:"ELOCKED"});await new Promise(w=>setTimeout(w,100))}const l=m?setInterval(()=>{try{const n=new Date;t.utimesSync(e,n,n)}catch{}},5e3):null;try{return await c()}finally{clearInterval(l);try{t.rmdirSync(e)}catch{}}}a(u,"withFileLock");export{u as withFileLock};
1
+ var f=Object.defineProperty;var o=(r,c)=>f(r,"name",{value:c,configurable:!0});import t from"node:fs";import a from"node:path";async function u(r,c,{staleAfter:i=3e4,timeout:m=1e4,heartbeat:s=!1}={}){const e=`${r}.lock`;t.mkdirSync(a.dirname(r),{recursive:!0});const y=performance.now()+m;for(;;)try{t.mkdirSync(e);break}catch(n){if(n.code!=="EEXIST")throw n;try{if(Date.now()-t.statSync(e).mtimeMs>i){t.rmdirSync(e);continue}}catch{}if(performance.now()>y)throw Object.assign(new Error(`${a.basename(r)} is locked by another kickflip; try again`),{code:"ELOCKED"});await new Promise(w=>setTimeout(w,100))}const l=s?setInterval(()=>{try{const n=new Date;t.utimesSync(e,n,n)}catch{}},5e3):null;try{return await c()}finally{clearInterval(l);try{t.rmdirSync(e)}catch{}}}o(u,"withFileLock");export{u as withFileLock};
package/hub/pr-report.mjs CHANGED
@@ -1,3 +1,3 @@
1
- var y=Object.defineProperty;var u=(t,n)=>y(t,"name",{value:n,configurable:!0});import{execFile as h}from"node:child_process";const _="state,isDraft,reviewDecision,mergeable,mergeStateStatus,headRefOid,statusCheckRollup",f="/opt/homebrew/bin:/usr/local/bin:/usr/bin:/bin:/usr/sbin:/sbin",g=u(t=>String(t).trim().split(/\r?\n/)[0].slice(0,200),"firstLine");function d(t,n=process.env){return new Promise((s,o)=>{h("gh",t,{env:{...n,PATH:[n.PATH,f].filter(Boolean).join(":")},timeout:9e4,maxBuffer:16*1024*1024},(l,i,m)=>{if(l)return o(new Error(g(m||l.message)));try{s(JSON.parse(i))}catch(a){o(new Error(g(`unreadable output (${a.message})`)))}})})}u(d,"gh");const R="\u{1F916} ",b="<!-- claude-bridge -->";async function x({repo:t,pr:n,replyTo:s,words:o,login:l}){let i=process.env;if(l){const a=await new Promise(r=>h("gh",["auth","token","-u",l],{env:{...i,PATH:[i.PATH,f].filter(Boolean).join(":")}},(c,e)=>r(c?"":e.trim())));if(!a)throw new Error(`gh has no ${l} account; run gh auth login`);i={...i,GH_TOKEN:a}}const m=s===void 0?`repos/${t}/issues/${n}/comments`:`repos/${t}/pulls/${n}/comments/${s}/replies`;try{return(await d(["api",m,"-f",`body=${R}${o}
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}
2
2
 
3
- ${b}`],i)).html_url}catch(a){throw new Error(`${t}#${n}: ${a.message}`)}}u(x,"postToPR");function C({repo:t,pr:n}){return new Promise((s,o)=>{h("gh",["pr","ready",String(n),"--repo",t],{env:{...process.env,PATH:[process.env.PATH,f].filter(Boolean).join(":")},timeout:9e4},(l,i,m)=>l?o(new Error(g(m||l.message))):s())})}u(C,"markReady");const D=u(t=>t.every(n=>n.__typename==="StatusContext"?n.state==="SUCCESS":n.status==="COMPLETED"&&["SUCCESS","NEUTRAL","SKIPPED"].includes(n.conclusion)),"checksPassed");async function A({repo:t,pr:n,since:s}){let o;try{o=await d(["pr","view",String(n),"--repo",t,"--json",_])}catch(r){throw new Error(`${t}#${n}: ${r.message}`)}const l=u(r=>d(["api","--paginate","--slurp",`repos/${t}/${r}`]).then(c=>c.flat()).catch(()=>[]),"list"),[i,m,a]=await Promise.all([l(`issues/${n}/comments?since=${encodeURIComponent(s)}`),l(`pulls/${n}/comments?since=${encodeURIComponent(s)}`),l(`pulls/${n}/reviews`)]);return{view:o,issueComments:i,reviewComments:m,reviews:a}}u(A,"fetchPR");const w="author { __typename login }",v=`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 ${w} createdAt body url } } reviews(last: 50) { nodes { databaseId ${w} submittedAt body state url } } reviewThreads(last: 50) { nodes { comments(first: 20) { nodes { databaseId ${w} createdAt body url path line replyTo { databaseId } } } } }`;function S(t,n=process.env){return new Promise((s,o)=>{h("gh",["api","graphql","-f",`query=${t}`],{env:{...n,PATH:[n.PATH,f].filter(Boolean).join(":")},timeout:9e4,maxBuffer:64*1024*1024},(l,i,m)=>{let a=null;try{a=JSON.parse(i)}catch{}if(a?.data)return s(a);o(new Error(`GitHub query: ${g(l?m||l.message:"unreadable output")}`))})})}u(S,"graphql");const $=u(t=>t?.login?{login:t.__typename==="Bot"?`${t.login}[bot]`:t.login}:null,"user");async function T(t){const n=t.map(({repo:i,pr:m},a)=>{const[r,c]=i.split("/");return`pr${a}: repository(owner: ${JSON.stringify(r)}, name: ${JSON.stringify(c)}) { pullRequest(number: ${Number(m)}) { ${v} } }`}),{data:s,errors:o=[]}=await S(`query { viewer { login } rateLimit { remaining resetAt } ${n.join(" ")} }`),l=t.map(({repo:i,pr:m},a)=>{const r=s[`pr${a}`]?.pullRequest;if(!r)return{error:`${i}#${m}: ${o.find(e=>e.path?.[0]===`pr${a}`)?.message||"GitHub did not answer for it"}`};const c=u(e=>({id:e.databaseId??null,user:$(e.author),created_at:e.createdAt??null,body:e.body??null,html_url:e.url??null}),"comment");return{view:{state:r.state,isDraft:r.isDraft,reviewDecision:r.reviewDecision,mergeable:r.mergeable,mergeStateStatus:r.mergeStateStatus,headRefOid:r.headRefOid,statusCheckRollup:r.commits?.nodes?.[0]?.commit?.statusCheckRollup?.contexts?.nodes??[]},issueComments:(r.comments?.nodes??[]).map(c),reviewComments:(r.reviewThreads?.nodes??[]).flatMap(e=>e.comments?.nodes??[]).map(e=>({...c(e),path:e.path??null,line:e.line??null,in_reply_to_id:e.replyTo?.databaseId??null})),reviews:(r.reviews?.nodes??[]).map(e=>({id:e.databaseId??null,user:$(e.author),submitted_at:e.submittedAt??null,body:e.body??null,state:e.state??null,html_url:e.url??null}))}});return{login:s.viewer?.login??null,rate:s.rateLimit??null,results:l}}u(T,"fetchPRs");function O({view:t,issueComments:n,reviewComments:s,reviews:o},{pr:l,since:i,me:m}){const a=u((e,p)=>({kind:p,id:e.id??null,login:e.user?.login??null,at:e.created_at??null,body:e.body??null}),"comment"),r=[...n.map(e=>({...a(e,"issue_comment"),url:e.html_url??null})),...s.map(e=>({...a(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>i).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,p)=>e.at<p.at?-1:e.at>p.at?1:0),c=t.statusCheckRollup??[];return{pr:Number(l),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:c.filter(e=>/FAILURE|ERROR/.test(e.conclusion??e.state??"")).map(e=>e.name??e.context??null),pending:c.filter(e=>(e.status??"COMPLETED")!=="COMPLETED").length,events:r}}u(O,"reportOf");async function I(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(o=>o.path),requested:(s.reviewRequests||[]).map(o=>o.login).filter(Boolean)}))}u(I,"openPRs");async function H(t,n){let s;try{s=await d(["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)}}u(H,"prFiles");async function L({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}u(L,"githubLogin");export{b as CLAUDE_MARK,R as CLAUDE_PREFIX,_ as PR_VIEW_FIELDS,D as checksPassed,A as fetchPR,T as fetchPRs,L as githubLogin,C as markReady,I as openPRs,x as postToPR,H as prFiles,O as reportOf};
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};
package/hub/presence.mjs CHANGED
@@ -1 +1 @@
1
- var f=Object.defineProperty;var o=(r,t)=>f(r,"name",{value:t,configurable:!0});import{execFile as p}from"node:child_process";const d=3*6e4,m=o(r=>new Promise((t,n)=>{p("ioreg",r,{timeout:5e3,maxBuffer:32*1024*1024},(e,a)=>e?n(e):t(a))}),"ioreg");async function I(r=m){try{const[t,n]=await Promise.all([r(["-n","Root","-d1"]),r(["-c","IOHIDSystem"])]),e=/"CGSSessionScreenIsLocked"\s*=\s*Yes/.test(t),a=[...n.matchAll(/"HIDIdleTime"\s*=\s*(\d+)/g)].map(i=>Number(i[1])/1e6);if(!a.length)return{atMac:!1,error:"ioreg reported no HIDIdleTime"};const s=Math.round(Math.min(...a));return{atMac:!e&&s<d,locked:e,idleMs:s}}catch(t){return{atMac:!1,error:String(t.message||t)}}}o(I,"readPresence");function g({run:r=m,ttl:t=5e3,clock:n=Date.now}={}){let e=null;return()=>((!e||n()-e.at>=t)&&(e={at:n(),reading:I(r)}),e.reading)}o(g,"presenceReader");const M=g();function w({read:r=M,clock:t=Date.now}={}){let n=-1/0,e=!1,a=null;const s=o(i=>{const l=t(),c=i||l-n<d,u={present:c,changed:c!==e,back:c&&!e};return u.back&&(a=l),e=c,u},"update");return{async check(){return s((await r()).atMac)},beat(){return n=t(),s(!0)},state:o(()=>({present:e,backAt:a}),"state")}}o(w,"ownerTracker");export{d as IDLE_LIMIT_MS,w as ownerTracker,M as presence,g as presenceReader,I as readPresence};
1
+ var m=Object.defineProperty;var a=(r,e)=>m(r,"name",{value:e,configurable:!0});import{execFile as f}from"node:child_process";const d=3*6e4,I=a(r=>new Promise((e,o)=>{f("ioreg",r,{timeout:5e3,maxBuffer:32*1024*1024},(t,n)=>t?o(t):e(n))}),"ioreg");async function p(r=I){try{const[e,o]=await Promise.all([r(["-n","Root","-d1"]),r(["-c","IOHIDSystem"])]),t=/"CGSSessionScreenIsLocked"\s*=\s*Yes/.test(e),n=[...o.matchAll(/"HIDIdleTime"\s*=\s*(\d+)/g)].map(i=>Number(i[1])/1e6);if(!n.length)return{atMac:!1,error:"ioreg reported no HIDIdleTime"};const s=Math.round(Math.min(...n));return{atMac:!t&&s<d,locked:t,idleMs:s}}catch(e){return{atMac:!1,error:String(e.message||e)}}}a(p,"readPresence");function h({read:r=p,clock:e=Date.now}={}){let o=-1/0,t=!1,n=null;const s=a(i=>{const l=e(),c=i||l-o<d,u={present:c,changed:c!==t,back:c&&!t};return u.back&&(n=l),t=c,u},"update");return{async check(){return s((await r()).atMac)},beat(){return o=e(),s(!0)},state:a(()=>({present:t,backAt:n}),"state")}}a(h,"ownerTracker");export{d as IDLE_LIMIT_MS,h as ownerTracker,p as readPresence};
package/hub/push.mjs CHANGED
@@ -1,2 +1,2 @@
1
- var G=Object.defineProperty;var r=(t,e)=>G(t,"name",{value:e,configurable:!0});import m from"node:crypto";import u from"node:fs";import y from"node:path";import x from"node:os";import{validate as Y,quietOf as C,flushAt as Z,hold as F,notifications as tt,snoozeEnd as et}from"./schedule.mjs";import{storedEnd as nt}from"./duration.mjs";import{presence as K}from"./presence.mjs";import{CODEX_SHARE_VALUES as ot,codexStatus as I,effectiveCodexShare as rt}from"../shared/codex.mjs";const j=y.join(x.homedir(),".claude","kickflip","push"),z=y.join(j,"vapid.json"),O=y.join(j,"subs.json"),h=y.join(j,"queue.json"),D=h+".lock",J=r(t=>y.join(t,".claude","kickflip","settings.json"),"settingsFile"),P=r(t=>Buffer.from(t).toString("base64url"),"b64u"),$=r(t=>Buffer.from(t,"base64url"),"unb64u"),B=r((t,e)=>m.createHmac("sha256",t).update(e).digest(),"hmac"),l=r((t,e)=>{u.mkdirSync(j,{recursive:!0,mode:448});const n=`${t}.${process.pid}.tmp`;u.writeFileSync(n,JSON.stringify(e,null,2),{mode:384}),u.renameSync(n,t)},"writePrivate");function L(){try{return JSON.parse(u.readFileSync(z,"utf8"))}catch{}const t=m.generateKeyPairSync("ec",{namedCurve:"prime256v1"}).privateKey.export({format:"jwk"});return l(z,t),t}r(L,"vapidKey");function st(){const t=L();return P(Buffer.concat([Buffer.from([4]),$(t.x),$(t.y)]))}r(st,"publicKey");function g(t=O){try{return JSON.parse(u.readFileSync(t,"utf8"))}catch{return[]}}r(g,"readSubs");function xt(t,e,n=""){if(!/^https:\/\//.test(t?.endpoint||"")||!t.keys?.p256dh||!t.keys?.auth)throw Object.assign(new Error("bad subscription"),{status:400});const o=g().filter(i=>i.endpoint!==t.endpoint);return o.push({endpoint:t.endpoint,keys:{p256dh:t.keys.p256dh,auth:t.keys.auth},origin:e,agent:String(n).slice(0,200),added:new Date().toISOString()}),l(O,o),o.length}r(xt,"addSubscription");function bt(t){const e=g(),n=e.filter(o=>o.endpoint!==t);return n.length!==e.length&&l(O,n),n.length}r(bt,"removeSubscription");function Et(t){return g(t?y.join(t,".claude","kickflip","push","subs.json"):O).map(e=>({host:new URL(e.endpoint).host,agent:e.agent,added:e.added}))}r(Et,"subscriptions");const M=r(t=>!t.agent||/iPhone|iPad|Android/.test(t.agent),"onPhone");async function T(t,e=!1){const n=g();if(t!=="action"||e||!(await K()).atMac)return n;const o=n.filter(i=>!M(i));return o.length?o:n}r(T,"reach");async function jt(){return{atMac:(await K()).atMac,desktops:g().filter(t=>!M(t)).length}}r(jt,"pushRoute");function it(t,e){const n=$(t.keys.p256dh),o=m.createECDH("prime256v1"),i=o.generateKeys(),a=B($(t.keys.auth),o.computeSecret(n)),s=B(a,Buffer.concat([Buffer.from("WebPush: info\0"),n,i,Buffer.from([1])])),c=m.randomBytes(16),d=B(c,s),k=B(d,Buffer.from("Content-Encoding: aes128gcm\0")).subarray(0,16),b=B(d,Buffer.from("Content-Encoding: nonce\0")).subarray(0,12),E=m.createCipheriv("aes-128-gcm",k,b),v=Buffer.concat([E.update(Buffer.concat([Buffer.from(e),Buffer.from([2])])),E.final(),E.getAuthTag()]),f=Buffer.alloc(4);return f.writeUInt32BE(4096),Buffer.concat([c,f,Buffer.from([i.length]),i,v])}r(it,"encrypt");function at(t){const e=L(),n=P(JSON.stringify({typ:"JWT",alg:"ES256"})),o=P(JSON.stringify({aud:new URL(t.endpoint).origin,exp:Math.floor(Date.now()/1e3)+12*3600,sub:t.origin||"mailto:kickflip@localhost"})),i=m.sign("sha256",Buffer.from(`${n}.${o}`),{key:m.createPrivateKey({key:e,format:"jwk"}),dsaEncoding:"ieee-p1363"});return`vapid t=${n}.${o}.${P(i)}, k=${st()}`}r(at,"authorization");async function R(){try{const t=await fetch(`http://127.0.0.1:${Number(process.env.KICKFLIP_HUB_PORT||4500)}/_hub/needs.json?fresh=1`,{signal:AbortSignal.timeout(4e3)}),{total:e,sites:n}=await t.json();return{total:Number.isInteger(e)?e:void 0,sites:n}}catch{return{}}}r(R,"hubNeeds");const Ot=r(t=>N(g(),t),"sendAll");async function N(t,e,n){if(!t.length)return[];const o=JSON.stringify({...e,badge:(n||await R()).total}),i=await Promise.all(t.map(async s=>{try{const c=await fetch(s.endpoint,{method:"POST",headers:{authorization:at(s),"content-encoding":"aes128gcm","content-type":"application/octet-stream",ttl:"86400",urgency:"high"},body:it(s,o),signal:AbortSignal.timeout(15e3)});return{sub:s,status:c.status,error:c.ok?void 0:(await c.text()).slice(0,300)}}catch(c){return{sub:s,status:0,error:String(c.message||c)}}})),a=new Set(i.filter(s=>s.status===404||s.status===410).map(s=>s.sub.endpoint));return a.size&&l(O,g().filter(s=>!a.has(s.endpoint))),i.map(s=>({host:new URL(s.sub.endpoint).host,agent:s.sub.agent,status:s.status,error:s.error,dropped:a.has(s.sub.endpoint)||void 0}))}r(N,"sendTo");const Q=r(t=>t.some(e=>e.status>=200&&e.status<300),"delivered");function w(t=x.homedir()){try{const e=JSON.parse(u.readFileSync(J(t),"utf8"));return e&&typeof e=="object"&&!Array.isArray(e)?e:{}}catch{return{}}}r(w,"readSettingsFile");function U(t,e=x.homedir()){const n=J(e);u.mkdirSync(y.dirname(n),{recursive:!0});const o=`${n}.${process.pid}.tmp`;u.writeFileSync(o,JSON.stringify(t,null,2)+`
2
- `),u.renameSync(o,n)}r(U,"writeSettingsFile");const Bt=r((t=I(),e=x.homedir())=>rt(w(e).codexShare,t),"readCodexShare");function vt(t,e=I()){if(!ot.includes(t))throw Object.assign(new Error("codex-share takes auto, always, or off"),{status:400});if(!e.usable)throw Object.assign(new Error(`${e.words} Run ${e.fix}, then choose again.`),{status:409});return U({...w(),codexShare:t}),t}r(vt,"setCodexShare");const H=["opus","auto","sonnet"],At=r((t=x.homedir())=>{const e=w(t).claudeModel;return H.includes(e)?e:"auto"},"readClaudeModel");function Dt(t,e=x.homedir()){if(!H.includes(t))throw Object.assign(new Error("claude-model takes opus, auto or sonnet"),{status:400});return U({...w(e),claudeModel:t},e),t}r(Dt,"setClaudeModel");function q(t){return C(w(t))}r(q,"readSettings");function Pt(t){const e=w(t);if(!e.polling&&!e.push)return null;const{push:n,polling:o,...i}=e,{quiet:a}=C(e);return U({...i,quiet:a},t),a}r(Pt,"migrateSettings");function ct(t){const{settings:e,error:n}=Y({notifications:q().notifications,...t});if(n)throw Object.assign(new Error(n),{status:400});const{push:o,polling:i,...a}=w();return U({...a,quiet:e.quiet,notifications:e.notifications}),e}r(ct,"saveSettings");async function $t(t,e=new Date){const n=q(),o={...n.notifications};if("on"in t&&(o.on=t.on),"weekends_off"in t&&(o.weekends_off=t.weekends_off),"snooze"in t){const s=t.snooze==="off"?null:et(e,t.snooze,n);if(s===null&&t.snooze!=="off")throw Object.assign(new Error("A snooze must end after now and within 30 days: <n>m, <n>h, <n>d, <n>w, tonight, morning, monday or a date and time; or forever or off."),{status:400});o.snoozed_until=s&&nt(s)}const{notifications:i}=ct({...n,notifications:o}),a=i.on?0:await S(()=>{const{items:s}=p();return s.length&&l(h,{items:[]}),s.length});return{notifications:i,dropped:a}}r($t,"setNotifications");function p(){try{const t=JSON.parse(u.readFileSync(h,"utf8"));return{items:Array.isArray(t.items)?t.items:[]}}catch{return{items:[]}}}r(p,"readQueue");async function S(t){u.mkdirSync(j,{recursive:!0,mode:448});const e=Date.now()+1e4;for(;;)try{u.mkdirSync(D);break}catch(n){if(n.code!=="EEXIST")throw n;try{if(Date.now()-u.statSync(D).mtimeMs>3e4){u.rmdirSync(D);continue}}catch{}if(Date.now()>e)throw new Error("push queue is locked; try again");await new Promise(o=>setTimeout(o,50))}try{return t()}finally{try{u.rmdirSync(D)}catch{}}}r(S,"withQueueLock");function ut(t=new Date){const e=q(),{items:n}=p(),o=F(t,e);return{settings:e,held:o!==null,hold:o,count:n.length,nextFlushAt:n.length?Z(t,e):null}}r(ut,"queueStatus");const V=["action","prompt","fault","reminder"];class ft extends Error{static{r(this,"PushKindError")}}const dt=r(t=>V.includes(t)?null:`a push needs --kind ${V.join("|")}${t===void 0||t===!0?"":`, not "${t}"`}; news goes on the board, not in a push`,"pushKindError");async function Ut(t,{kind:e,urgent:n=!1,immediate:o=!1,at:i=new Date}={}){const a=dt(e);if(a)throw new ft(a);if(!g().length)return{noDevice:!0};const s={title:t.title,body:t.body||"",url:t.url||"/",tag:t.tag||void 0},c=F(i,q());if(c?.why==="off")return{dropped:!0};const d=c!==null;let k=!1;if(n||o&&!d){const f=await N(await T(e,n),s);if(Q(f))return s.tag&&await S(()=>l(h,{items:p().items.filter(A=>A.tag!==s.tag)})),{sent:f};if(n)return{sent:f};k=!0}const b={id:m.randomUUID(),...s,kind:e,queuedAt:i.toISOString()},E=await S(()=>{const f=p(),A=b.tag?f.items.filter(X=>X.tag!==b.tag):f.items,W=A.length<f.items.length;return f.items=[...A,b],l(h,f),{replaced:W}}),v=ut(i);return{queued:{...E,count:v.count,nextFlushAt:v.nextFlushAt,held:d,hold:c,sendFailed:k}}}r(Ut,"deliver");async function qt(t,e,n){return!t||!p().items.some(o=>o.tag===t)?null:S(()=>{const o=p(),i=o.items.find(a=>a.tag===t);return i?(i.body=e(i.body||""),n&&(i.url=n),l(h,o),i.body):null})}r(qt,"reviseQueued");async function Nt(t,e){const n=new Set(t),o=r(i=>n.has(i.tag)&&typeof i.url=="string"&&i.url.startsWith(`/${e}/`),"matches");return p().items.some(o)?S(()=>{const{items:i}=p(),a=i.filter(c=>!o(c)),s=i.length-a.length;return s&&l(h,{items:a}),s}):0}r(Nt,"dropQueued");let _=null;function _t(){return _||=(async()=>{const t=await S(()=>{const a=p();return a.items.length&&l(h,{items:[]}),a.items});if(!t.length)return null;const e=[],n=[],o=[],i=await R();for(const a of tt(t,i.sites)){const s=await N(await T(a.kind),{title:a.title,body:a.body,url:a.url,tag:a.tag},i);o.push(...s),(Q(s)?e:n).push(a)}return n.length&&await S(()=>{const a=p(),s=new Set(a.items.map(d=>d.tag).filter(Boolean)),c=t.filter(d=>n.some(k=>k.members.includes(d.id))&&!(d.tag&&s.has(d.tag)));l(h,{items:[...c,...a.items]})}),{count:t.length,sent:e.length,ok:!n.length,results:o}})().finally(()=>{_=null}),_}r(_t,"flushQueue");export{H as CLAUDE_MODEL_VALUES,V as PUSH_KINDS,ft as PushKindError,xt as addSubscription,Ut as deliver,Nt as dropQueued,_t as flushQueue,Pt as migrateSettings,st as publicKey,dt as pushKindError,jt as pushRoute,ut as queueStatus,At as readClaudeModel,Bt as readCodexShare,q as readSettings,w as readSettingsFile,bt as removeSubscription,qt as reviseQueued,ct as saveSettings,Ot as sendAll,Dt as setClaudeModel,vt as setCodexShare,$t as setNotifications,Et as subscriptions,U as writeSettingsFile};
1
+ var V=Object.defineProperty;var r=(t,e)=>V(t,"name",{value:e,configurable:!0});import h from"node:crypto";import u from"node:fs";import y from"node:path";import k from"node:os";import{validate as X,quietOf as _,flushAt as G,hold as I,notifications as Y,snoozeEnd as Z}from"./schedule.mjs";import{storedEnd as tt}from"./duration.mjs";import{CODEX_SHARE_VALUES as et,codexStatus as K,effectiveCodexShare as nt}from"../shared/codex.mjs";const j=y.join(k.homedir(),".claude","kickflip","push"),L=y.join(j,"vapid.json"),b=y.join(j,"subs.json"),m=y.join(j,"queue.json"),C=m+".lock",z=r(t=>y.join(t,".claude","kickflip","settings.json"),"settingsFile"),F=r(t=>Buffer.from(t).toString("base64url"),"b64u"),D=r(t=>Buffer.from(t,"base64url"),"unb64u"),v=r((t,e)=>h.createHmac("sha256",t).update(e).digest(),"hmac"),f=r((t,e)=>{u.mkdirSync(j,{recursive:!0,mode:448});const n=`${t}.${process.pid}.tmp`;u.writeFileSync(n,JSON.stringify(e,null,2),{mode:384}),u.renameSync(n,t)},"writePrivate");function J(){try{return JSON.parse(u.readFileSync(L,"utf8"))}catch{}const t=h.generateKeyPairSync("ec",{namedCurve:"prime256v1"}).privateKey.export({format:"jwk"});return f(L,t),t}r(J,"vapidKey");function ot(){const t=J();return F(Buffer.concat([Buffer.from([4]),D(t.x),D(t.y)]))}r(ot,"publicKey");function g(t=b){try{return JSON.parse(u.readFileSync(t,"utf8"))}catch{return[]}}r(g,"readSubs");function St(t,e,n=""){if(!/^https:\/\//.test(t?.endpoint||"")||!t.keys?.p256dh||!t.keys?.auth)throw Object.assign(new Error("bad subscription"),{status:400});const o=g().filter(i=>i.endpoint!==t.endpoint);return o.push({endpoint:t.endpoint,keys:{p256dh:t.keys.p256dh,auth:t.keys.auth},origin:e,agent:String(n).slice(0,200),added:new Date().toISOString()}),f(b,o),{count:o.length,id:N(t.endpoint)}}r(St,"addSubscription");const N=r(t=>h.createHash("sha256").update(t).digest("hex").slice(0,16),"deviceId");function xt(t){const e=g(),n=e.filter(o=>N(o.endpoint)!==t);return n.length!==e.length&&f(b,n),n.length}r(xt,"removeSubscriptionById");function kt(t){const e=g(),n=e.filter(o=>o.endpoint!==t);return n.length!==e.length&&f(b,n),n.length}r(kt,"removeSubscription");function rt(t=""){const e=["iPhone","iPad","Android","Windows","Macintosh","Linux"].find(o=>t.includes(o)),n=[["Edg/","Edge"],["OPR/","Opera"],["Firefox/","Firefox"],["FxiOS/","Firefox"],["CriOS/","Chrome"],["Chrome/","Chrome"],["Safari/","Safari"]].find(([o])=>t.includes(o));return{kind:e==="Macintosh"?"Mac":e||"",browser:n?.[1]||""}}r(rt,"deviceNames");function bt(t){return g(t?y.join(t,".claude","kickflip","push","subs.json"):b).map(e=>({id:N(e.endpoint),host:new URL(e.endpoint).host,agent:e.agent,added:e.added,...rt(e.agent)}))}r(bt,"subscriptions");function st(t,e){const n=D(t.keys.p256dh),o=h.createECDH("prime256v1"),i=o.generateKeys(),c=v(D(t.keys.auth),o.computeSecret(n)),s=v(c,Buffer.concat([Buffer.from("WebPush: info\0"),n,i,Buffer.from([1])])),a=h.randomBytes(16),l=v(a,s),x=v(l,Buffer.from("Content-Encoding: aes128gcm\0")).subarray(0,16),E=v(l,Buffer.from("Content-Encoding: nonce\0")).subarray(0,12),O=h.createCipheriv("aes-128-gcm",x,E),B=Buffer.concat([O.update(Buffer.concat([Buffer.from(e),Buffer.from([2])])),O.final(),O.getAuthTag()]),d=Buffer.alloc(4);return d.writeUInt32BE(4096),Buffer.concat([a,d,Buffer.from([i.length]),i,B])}r(st,"encrypt");function it(t){const e=J(),n=F(JSON.stringify({typ:"JWT",alg:"ES256"})),o=F(JSON.stringify({aud:new URL(t.endpoint).origin,exp:Math.floor(Date.now()/1e3)+12*3600,sub:t.origin||"mailto:kickflip@localhost"})),i=h.sign("sha256",Buffer.from(`${n}.${o}`),{key:h.createPrivateKey({key:e,format:"jwk"}),dsaEncoding:"ieee-p1363"});return`vapid t=${n}.${o}.${F(i)}, k=${ot()}`}r(it,"authorization");async function M(){try{const t=await fetch(`http://127.0.0.1:${Number(process.env.KICKFLIP_HUB_PORT||4500)}/_hub/needs.json?fresh=1`,{signal:AbortSignal.timeout(4e3)}),{total:e,sites:n}=await t.json();return{total:Number.isInteger(e)?e:void 0,sites:n}}catch{return{}}}r(M,"hubNeeds");const Et=r(t=>U(g(),t),"sendAll");async function U(t,e,n){if(!t.length)return[];const o=JSON.stringify({...e,badge:(n||await M()).total}),i=await Promise.all(t.map(async s=>{try{const a=await fetch(s.endpoint,{method:"POST",headers:{authorization:it(s),"content-encoding":"aes128gcm","content-type":"application/octet-stream",ttl:"86400",urgency:"high"},body:st(s,o),signal:AbortSignal.timeout(15e3)});return{sub:s,status:a.status,error:a.ok?void 0:(await a.text()).slice(0,300)}}catch(a){return{sub:s,status:0,error:String(a.message||a)}}})),c=new Set(i.filter(s=>s.status===404||s.status===410).map(s=>s.sub.endpoint));return c.size&&f(b,g().filter(s=>!c.has(s.endpoint))),i.map(s=>({host:new URL(s.sub.endpoint).host,agent:s.sub.agent,status:s.status,error:s.error,dropped:c.has(s.sub.endpoint)||void 0}))}r(U,"sendTo");const T=r(t=>t.some(e=>e.status>=200&&e.status<300),"delivered");function w(t=k.homedir()){try{const e=JSON.parse(u.readFileSync(z(t),"utf8"));return e&&typeof e=="object"&&!Array.isArray(e)?e:{}}catch{return{}}}r(w,"readSettingsFile");function P(t,e=k.homedir()){const n=z(e);u.mkdirSync(y.dirname(n),{recursive:!0});const o=`${n}.${process.pid}.tmp`;u.writeFileSync(o,JSON.stringify(t,null,2)+`
2
+ `),u.renameSync(o,n)}r(P,"writeSettingsFile");const Ot=r((t=K(),e=k.homedir())=>nt(w(e).codexShare,t),"readCodexShare");function jt(t,e=K()){if(!et.includes(t))throw Object.assign(new Error("codex-share takes auto, always, or off"),{status:400});if(!e.usable)throw Object.assign(new Error(`${e.words} Run ${e.fix}, then choose again.`),{status:409});return P({...w(),codexShare:t}),t}r(jt,"setCodexShare");const R=["opus","auto","sonnet"],vt=r((t=k.homedir())=>{const e=w(t).claudeModel;return R.includes(e)?e:"auto"},"readClaudeModel");function Bt(t,e=k.homedir()){if(!R.includes(t))throw Object.assign(new Error("claude-model takes opus, auto or sonnet"),{status:400});return P({...w(e),claudeModel:t},e),t}r(Bt,"setClaudeModel");function $(t){return _(w(t))}r($,"readSettings");function At(t){const e=w(t);if(!e.polling&&!e.push)return null;const{push:n,polling:o,...i}=e,{quiet:c}=_(e);return P({...i,quiet:c},t),c}r(At,"migrateSettings");function ct(t){const{settings:e,error:n}=X({notifications:$().notifications,...t});if(n)throw Object.assign(new Error(n),{status:400});const{push:o,polling:i,...c}=w();return P({...c,quiet:e.quiet,notifications:e.notifications}),e}r(ct,"saveSettings");async function Ct(t,e=new Date){const n=$(),o={...n.notifications};if("on"in t&&(o.on=t.on),"weekends_off"in t&&(o.weekends_off=t.weekends_off),"snooze"in t){const s=t.snooze==="off"?null:Z(e,t.snooze,n);if(s===null&&t.snooze!=="off")throw Object.assign(new Error("A snooze must end after now and within 30 days: <n>m, <n>h, <n>d, <n>w, tonight, morning, monday or a date and time; or forever or off."),{status:400});o.snoozed_until=s&&tt(s)}const{notifications:i}=ct({...n,notifications:o}),c=i.on?0:await S(()=>{const{items:s}=p();return s.length&&f(m,{items:[]}),s.length});return{notifications:i,dropped:c}}r(Ct,"setNotifications");function p(){try{const t=JSON.parse(u.readFileSync(m,"utf8"));return{items:Array.isArray(t.items)?t.items:[]}}catch{return{items:[]}}}r(p,"readQueue");async function S(t){u.mkdirSync(j,{recursive:!0,mode:448});const e=Date.now()+1e4;for(;;)try{u.mkdirSync(C);break}catch(n){if(n.code!=="EEXIST")throw n;try{if(Date.now()-u.statSync(C).mtimeMs>3e4){u.rmdirSync(C);continue}}catch{}if(Date.now()>e)throw new Error("push queue is locked; try again");await new Promise(o=>setTimeout(o,50))}try{return t()}finally{try{u.rmdirSync(C)}catch{}}}r(S,"withQueueLock");function at(t=new Date){const e=$(),{items:n}=p(),o=I(t,e);return{settings:e,held:o!==null,hold:o,count:n.length,nextFlushAt:n.length?G(t,e):null}}r(at,"queueStatus");const Q=["action","prompt","fault","reminder"];class ut extends Error{static{r(this,"PushKindError")}}const dt=r(t=>Q.includes(t)?null:`a push needs --kind ${Q.join("|")}${t===void 0||t===!0?"":`, not "${t}"`}; news goes on the board, not in a push`,"pushKindError");async function Ft(t,{kind:e,urgent:n=!1,immediate:o=!1,at:i=new Date}={}){const c=dt(e);if(c)throw new ut(c);if(!g().length)return{noDevice:!0};const s={title:t.title,body:t.body||"",url:t.url||"/",tag:t.tag||void 0},a=I(i,$());if(a?.why==="off")return{dropped:!0};const l=a!==null;let x=!1;if(n||o&&!l){const d=await U(g(),s);if(T(d))return s.tag&&await S(()=>f(m,{items:p().items.filter(A=>A.tag!==s.tag)})),{sent:d};if(n)return{sent:d};x=!0}const E={id:h.randomUUID(),...s,kind:e,queuedAt:i.toISOString()},O=await S(()=>{const d=p(),A=E.tag?d.items.filter(W=>W.tag!==E.tag):d.items,H=A.length<d.items.length;return d.items=[...A,E],f(m,d),{replaced:H}}),B=at(i);return{queued:{...O,count:B.count,nextFlushAt:B.nextFlushAt,held:l,hold:a,sendFailed:x}}}r(Ft,"deliver");async function Dt(t,e,n){return!t||!p().items.some(o=>o.tag===t)?null:S(()=>{const o=p(),i=o.items.find(c=>c.tag===t);return i?(i.body=e(i.body||""),n&&(i.url=n),f(m,o),i.body):null})}r(Dt,"reviseQueued");async function Pt(t,e){const n=new Set(t),o=r(i=>n.has(i.tag)&&typeof i.url=="string"&&i.url.startsWith(`/${e}/`),"matches");return p().items.some(o)?S(()=>{const{items:i}=p(),c=i.filter(a=>!o(a)),s=i.length-c.length;return s&&f(m,{items:c}),s}):0}r(Pt,"dropQueued");let q=null;function $t(){return q||=(async()=>{const t=await S(()=>{const c=p();return c.items.length&&f(m,{items:[]}),c.items});if(!t.length)return null;const e=[],n=[],o=[],i=await M();for(const c of Y(t,i.sites)){const s=await U(g(),{title:c.title,body:c.body,url:c.url,tag:c.tag},i);o.push(...s),(T(s)?e:n).push(c)}return n.length&&await S(()=>{const c=p(),s=new Set(c.items.map(l=>l.tag).filter(Boolean)),a=t.filter(l=>n.some(x=>x.members.includes(l.id))&&!(l.tag&&s.has(l.tag)));f(m,{items:[...a,...c.items]})}),{count:t.length,sent:e.length,ok:!n.length,results:o}})().finally(()=>{q=null}),q}r($t,"flushQueue");export{R as CLAUDE_MODEL_VALUES,Q as PUSH_KINDS,ut as PushKindError,St as addSubscription,Ft as deliver,rt as deviceNames,Pt as dropQueued,$t as flushQueue,At as migrateSettings,ot as publicKey,dt as pushKindError,at as queueStatus,vt as readClaudeModel,Ot as readCodexShare,$ as readSettings,w as readSettingsFile,kt as removeSubscription,xt as removeSubscriptionById,Dt as reviseQueued,ct as saveSettings,Et as sendAll,Bt as setClaudeModel,jt as setCodexShare,Ct as setNotifications,bt as subscriptions,P as writeSettingsFile};
package/hub/readiness.mjs CHANGED
@@ -1,2 +1 @@
1
- var T=Object.defineProperty;var i=(t,n)=>T(t,"name",{value:n,configurable:!0});import f from"node:fs";import m from"node:os";import y from"node:path";import{execFile as N}from"node:child_process";import{githubLogin as P}from"./pr-report.mjs";import{codexStatusReader as w}from"../shared/codex.mjs";import{linearStatus as R}from"./linear.mjs";import{readSettings as C,subscriptions as O}from"./push.mjs";import{tailscaleState as I}from"../bin/install.mjs";import{tailscaleBin as L,tailscaleInstallCommand as F}from"../shared/tailscale.mjs";const x=[["github","GitHub","GitHub polling and PR reports wait on this."],["codex","Codex","Codex implementing and reviewing wait on this; Codex share stays Off."],["linear","Linear",null],["slack","Slack","Reviewer DMs and digests wait on this."],["tailscale","Tailscale","Try builds wait on this."],["notifications","Notifications","Pushes wait on this."]],k=i(t=>y.join(t,".claude","kickflip","integrations.json"),"reportFile");function j(t=m.homedir()){try{return JSON.parse(f.readFileSync(k(t),"utf8")).slack||null}catch{return null}}i(j,"readSlackReport");function W(t,{home:n=m.homedir(),now:s=new Date,route:d=null,title:o="Board"}={}){const e=k(n),r={ready:t,at:s.toISOString(),route:d,title:o};f.mkdirSync(y.dirname(e),{recursive:!0});const a=`${e}.${process.pid}.tmp`;return f.writeFileSync(a,JSON.stringify({slack:r},null,2)+`
2
- `,{mode:384}),f.renameSync(a,e),r}i(W,"reportSlack");const D=i((t,n)=>{const s=Math.floor((n-Date.parse(t))/6e4);return s<1?"just now":s<60?`${s} min ago`:s<1440?`${Math.floor(s/60)} h ago`:`${Math.floor(s/1440)} days ago`},"age"),u=i((t,n,s)=>({settings:t,label:`Hub settings \u203A ${n}`,text:s}),"settingsFix");function $(t,{now:n=Date.now(),platform:s=process.platform}={}){const d=i((o,e)=>{switch(o){case"github":return e?{ready:!0,detail:`gh logged in as ${e}`}:{ready:!1,missing:"gh isn't logged in on the host",fix:{command:"gh auth login"}};case"codex":return e.usable?{ready:!0,detail:"Installed and logged in"}:{ready:!1,missing:e.words,fix:{command:e.fix}};case"linear":return e.set?e.refused?{ready:!1,missing:"Linear refused the key",fix:u("linear","Linear","Paste a new one in")}:{ready:!0,detail:`Key set \xB7 ${e.workspace} workspace`}:{ready:!1,missing:"No Linear key set",fix:u("linear","Linear","Add one in")};case"slack":{const r=e?`${e.title}\u2019s session reported ${e.ready?"a":"no"} Slack connector, ${D(e.at,n)}`:"No board session has reported yet";return e?.ready?{ready:!0,detail:r}:{ready:!1,missing:r,fix:{text:"Add Slack to Claude\u2019s connectors. The next board session to start reports it."}}}case"tailscale":{let r,a;return e.installed?e.signedIn?e.pointsAtHub?e.operatorChecked&&e.operator!==e.username&&(r=`Tailscale operator is ${e.operator||"none"} (expected ${e.username})`,a="sudo tailscale set --operator=$USER"):(r="Tailscale / is not serving the hub",a="kickflip doctor --fix"):(r="Tailscale isn't signed in",a="tailscale up"):(r="Tailscale isn't installed on the host",a=F(s)),r?{ready:!1,missing:r,fix:{command:a}}:{ready:!0,detail:`Serving ${e.dns}${e.operatorChecked?" \xB7 operator "+e.operator:""}`}}case"notifications":return e.on&&e.devices?{ready:!0,detail:`Pushes on \xB7 ${e.devices} device${e.devices===1?"":"s"}`}:{ready:!1,missing:e.on?"No device has turned on notifications":"Pushes are off",fix:u("notifications","Notifications","Turn on notifications in")}}},"row");return x.filter(([o])=>Object.hasOwn(t,o)).map(([o,e,r])=>({id:o,name:e,...d(o,t[o]),waits:r}))}i($,"readinessRows");const H=i(t=>(n,s)=>new Promise(d=>{N(n,s,{env:t,timeout:3e3},(o,e)=>d({status:o?1:0,stdout:e}))}),"asyncRun");function A({home:t=m.homedir(),env:n=process.env,now:s=Date.now,cacheMs:d=6e4,probes:o={}}={}){const e=w({home:t,env:n,now:s,cacheMs:d}),r={github:i(()=>P({env:n}).catch(()=>null),"github"),codex:e,linear:i(()=>R(t),"linear"),slack:i(()=>j(t),"slack"),tailscale:i(()=>I({run:H(n),port:Number(n.KICKFLIP_HUB_PORT||4500),tailscale:L({env:n})}),"tailscale"),notifications:i(()=>({on:C(t).notifications.on,devices:O(t).length}),"notifications"),...o},a=new Map;async function g(l,c){if(!["github","tailscale"].includes(l))return r[l]({fresh:c});const p=a.get(l);if(!c&&p&&s()-p.at<d)return p.value;const h=Promise.resolve().then(()=>r[l]());return a.set(l,{at:s(),value:h}),h}i(g,"check");const b=i(async({fresh:l=!1}={})=>$(Object.fromEntries(await Promise.all(x.map(async([c])=>[c,await g(c,l)]))),{now:s()}),"rows");async function S(l){const[c]=$({[l]:await g(l,!1)},{now:s()});return c.ready||!c.waits?null:c.waits.replace("this",`${c.name}: ${c.missing.replace(/\.$/,"")}`)}return i(S,"waiting"),{rows:b,waiting:S}}i(A,"readinessReader");const M=w(),X=A({probes:{codex:M}});export{M as readCodex,j as readSlackReport,X as readiness,A as readinessReader,$ as readinessRows,W as reportSlack};
1
+ var k=Object.defineProperty;var t=(s,n)=>k(s,"name",{value:n,configurable:!0});import b from"node:os";import{execFile as $}from"node:child_process";import{githubLogin as S}from"./pr-report.mjs";import{codexStatusReader as h}from"../shared/codex.mjs";import{linearStatus as P}from"./linear.mjs";import{slackStatus as T}from"./slack.mjs";import{readSettings as R,subscriptions as C}from"./push.mjs";import{tailscaleState as N}from"../bin/install.mjs";import{tailscaleBin as I,tailscaleInstallCommand as L}from"../shared/tailscale.mjs";const w=[["github","GitHub","GitHub polling and PR reports wait on this."],["codex","Codex","Codex implementing and reviewing wait on this; Codex share stays Off."],["linear","Linear",null],["slack","Slack","Reviewer DMs and digests wait on this."],["tailscale","Tailscale","Try builds wait on this."],["notifications","Notifications","Pushes wait on this."]],f=t((s,n,a)=>({settings:s,label:`Hub settings \u203A ${n}`,text:a}),"settingsFix");function x(s,{now:n=Date.now(),platform:a=process.platform}={}){const d=t((o,e)=>{switch(o){case"github":return e?{ready:!0,detail:`gh logged in as ${e}`}:{ready:!1,missing:"gh isn't logged in on the host",fix:{command:"gh auth login"}};case"codex":return e.usable?{ready:!0,detail:"Installed and logged in"}:{ready:!1,missing:e.words,fix:{command:e.fix}};case"linear":return e.set?e.refused?{ready:!1,missing:"Linear refused the key",fix:f("linear","Linear","Paste a new one in")}:{ready:!0,detail:`Key set \xB7 ${e.workspace} workspace`}:{ready:!1,missing:"No Linear key set",fix:f("linear","Linear","Add one in")};case"slack":return e.set?e.refused?{ready:!1,missing:"Slack refused the token",fix:f("slack","Slack","Paste a new one in")}:{ready:!0,detail:`Token set \xB7 ${e.team} workspace, sending as ${e.user}`}:{ready:!1,missing:"No Slack token set",fix:f("slack","Slack","Add one in")};case"tailscale":{let i,c;return e.installed?e.signedIn?e.pointsAtHub?e.operatorChecked&&e.operator!==e.username&&(i=`Tailscale operator is ${e.operator||"none"} (expected ${e.username})`,c="sudo tailscale set --operator=$USER"):(i="Tailscale / is not serving the hub",c="kickflip doctor --fix"):(i="Tailscale isn't signed in",c="tailscale up"):(i="Tailscale isn't installed on the host",c=L(a)),i?{ready:!1,missing:i,fix:{command:c}}:{ready:!0,detail:`Serving ${e.dns}${e.operatorChecked?" \xB7 operator "+e.operator:""}`}}case"notifications":return e.on&&e.devices.length?{ready:!0,detail:`Pushes on \xB7 ${e.devices.length} device${e.devices.length===1?"":"s"}`,devices:e.devices.map(({id:i,kind:c,browser:m,added:p})=>({id:i,kind:c,browser:m,added:p}))}:{ready:!1,missing:e.on?"No device has turned on notifications":"Pushes are off",fix:f("notifications","Notifications","Turn on notifications in")}}},"row");return w.filter(([o])=>Object.hasOwn(s,o)).map(([o,e,i])=>({id:o,name:e,...d(o,s[o]),waits:i}))}t(x,"readinessRows");const O=t(s=>(n,a)=>new Promise(d=>{$(n,a,{env:s,timeout:3e3},(o,e)=>d({status:o?1:0,stdout:e}))}),"asyncRun");function H({home:s=b.homedir(),env:n=process.env,now:a=Date.now,cacheMs:d=6e4,probes:o={}}={}){const e=h({home:s,env:n,now:a,cacheMs:d}),i={github:t(()=>S({env:n}).catch(()=>null),"github"),codex:e,linear:t(()=>P(s),"linear"),slack:t(()=>T(s),"slack"),tailscale:t(()=>N({run:O(n),port:Number(n.KICKFLIP_HUB_PORT||4500),tailscale:I({env:n})}),"tailscale"),notifications:t(()=>({on:R(s).notifications.on,devices:C(s)}),"notifications"),...o},c=new Map;async function m(l,r){if(!["github","tailscale"].includes(l))return i[l]({fresh:r});const g=c.get(l);if(!r&&g&&a()-g.at<d)return g.value;const u=Promise.resolve().then(()=>i[l]());return c.set(l,{at:a(),value:u}),u}t(m,"check");const p=t(async({fresh:l=!1}={})=>x(Object.fromEntries(await Promise.all(w.map(async([r])=>[r,await m(r,l)]))),{now:a()}),"rows");async function y(l){const[r]=x({[l]:await m(l,!1)},{now:a()});return r.ready||!r.waits?null:r.waits.replace("this",`${r.name}: ${r.missing.replace(/\.$/,"")}`)}return t(y,"waiting"),{rows:p,waiting:y}}t(H,"readinessReader");const A=h(),q=H({probes:{codex:A}});export{A as readCodex,q as readiness,H as readinessReader,x as readinessRows};
@@ -0,0 +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};
@@ -0,0 +1,2 @@
1
+ var d=Object.defineProperty;var c=(r,e)=>d(r,"name",{value:e,configurable:!0});import n from"node:fs";import o from"node:path";import m from"node:os";const l=c((r=m.homedir())=>o.join(r,".claude","kickflip"),"dirOf"),a=c(r=>o.join(l(r),"secrets.json"),"secretsFile");function f(r,e){n.mkdirSync(o.dirname(r),{recursive:!0});const t=`${r}.${process.pid}.tmp`;n.writeFileSync(t,JSON.stringify(e,null,2)+`
2
+ `,{mode:384}),n.chmodSync(t,384),n.renameSync(t,r)}c(f,"write");function p(r){const e=a(r);let t={};try{t=JSON.parse(n.readFileSync(e,"utf8"))||{}}catch{}const i=o.join(l(r),"linear.json");if(n.existsSync(i)){let s=null;try{s=JSON.parse(n.readFileSync(i,"utf8"))}catch{}!t.linear&&s&&(t={...t,linear:s},f(e,t)),n.rmSync(i,{force:!0})}return t}c(p,"readAll");const h=c((r,e)=>p(e)[r]||null,"readSecret");function x(r,e){const{[r]:t,...i}=p();f(a(),e?{...i,[r]:e}:i)}c(x,"writeSecret");export{h as readSecret,a as secretsFile,x as writeSecret};
@@ -0,0 +1,2 @@
1
+ var y=Object.defineProperty;var e=(n,i)=>y(n,"name",{value:i,configurable:!0});import d from"node:fs";import h from"node:path";import{execFileSync as b}from"node:child_process";const g=["index.lock","MERGE_HEAD","rebase-merge","rebase-apply","CHERRY_PICK_HEAD","REVERT_HEAD"];function w({root:n,files:i,restart:p,log:s=e(()=>{},"log"),git:c=e((...r)=>b("git",r,{cwd:n,encoding:"utf8"}),"git")}){const r=d.existsSync(h.join(n,".git"));let a=!1;function u(){try{const t=c("rev-parse","HEAD").trim();return a=!1,t}catch(t){return a||s(`self-restart can't read HEAD: ${t.message.split(`
2
+ `)[0]}`),a=!0,null}}e(u,"head");const f=r?u():null;let l=null;function o(t,R){if(l===t)return;l=t,s(`restarting the hub: ${R}`);const m=p();m&&s(`hub restart failed: ${m}`)}e(o,"attempt");const E=e(()=>g.some(t=>d.existsSync(h.join(n,".git",t))),"gitBusy"),A=e(()=>{try{return c("diff","--quiet","HEAD","--",...i),!0}catch{return!1}},"filesMatchHead");function H(){if(!r||E())return;const t=u();t===null||f===null||t===f||!A()||o(t,`HEAD moved to ${t.slice(0,7)} and the hub's files match it`)}e(H,"check");function D(){o(`npm-${Date.now()}`,"kickflip update finished")}return e(D,"restartNow"),{check:H,restartNow:D}}e(w,"restarter");export{w as restarter};