@lovek4d/kickflip 0.3.1 → 0.5.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (111) hide show
  1. package/CONTEXT.md +170 -118
  2. package/README.md +23 -14
  3. package/SKILL.md +40 -38
  4. package/bin/doctor.mjs +4 -4
  5. package/bin/install.mjs +3 -3
  6. package/bin/kickflip.mjs +34 -26
  7. package/bin/wait-events.mjs +2 -2
  8. package/board/arch-review.mjs +1 -1
  9. package/board/auto-review.mjs +2 -0
  10. package/board/bin/board.mjs +213 -74
  11. package/board/domain.mjs +8 -5
  12. package/board/finish.mjs +4 -0
  13. package/board/headless.mjs +4 -0
  14. package/board/merge3.mjs +1 -0
  15. package/board/on-host.mjs +1 -1
  16. package/board/permissions.mjs +2 -0
  17. package/board/public/app-arch.js +2 -2
  18. package/board/public/app-asks.js +39 -103
  19. package/board/public/app-banner.js +2 -2
  20. package/board/public/app-cards.js +166 -73
  21. package/board/public/app-core.js +2 -23
  22. package/board/public/app-header.js +12 -5
  23. package/board/public/app-lanes.js +201 -47
  24. package/board/public/app-needs.js +2 -1
  25. package/board/public/app-reader.js +174 -0
  26. package/board/public/app-snapshot.js +2 -2
  27. package/board/public/app-threads.js +53 -42
  28. package/board/public/app.html +4 -1
  29. package/board/public/app.js +78 -96
  30. package/board/public/attach.js +39 -3
  31. package/board/public/dialogs.js +59 -14
  32. package/board/public/hub-kit.js +9 -6
  33. package/board/public/hub.css +6 -18
  34. package/board/public/linear-key.js +6 -4
  35. package/board/public/log.html +48 -0
  36. package/board/public/new-task.js +121 -0
  37. package/board/public/prompts.js +9 -8
  38. package/board/public/prototype-kit.js +6 -6
  39. package/board/public/prototype-switcher.js +13 -5
  40. package/board/public/prototype.html +1 -0
  41. package/board/public/router.js +8 -7
  42. package/board/public/rules-editor.js +3 -4
  43. package/board/public/shell.js +24 -6
  44. package/board/public/sidebar.css +22 -1
  45. package/board/public/sidebar.js +64 -31
  46. package/board/public/style.css +245 -64
  47. package/board/public/toasts.js +2 -2
  48. package/board/public/ui-cards.js +205 -75
  49. package/board/public/ui-enums.js +6 -5
  50. package/board/public/ui-header.js +10 -10
  51. package/board/public/ui-hub.js +4 -4
  52. package/board/public/ui-markdown.js +1 -1
  53. package/board/public/ui-order.js +9 -6
  54. package/board/public/ui-page-header.js +10 -7
  55. package/board/public/ui-patch.js +53 -0
  56. package/board/public/ui-phase-bars.js +17 -0
  57. package/board/public/ui-prompts.js +8 -8
  58. package/board/public/ui-questions.js +61 -29
  59. package/board/public/ui-sidebar.js +54 -24
  60. package/board/public/ui-snapshot.js +20 -9
  61. package/board/public/ui-threads.js +40 -21
  62. package/board/public/ui.js +116 -18
  63. package/board/public/view-frame.js +2 -1
  64. package/board/public/view-hub-settings.js +25 -18
  65. package/board/public/view-hub.js +53 -12
  66. package/board/public/view-notes.js +1 -1
  67. package/board/public/view-questions.js +142 -62
  68. package/board/public/view-settings.js +189 -52
  69. package/board/review.mjs +1 -1
  70. package/board/server.mjs +20 -12
  71. package/board/store.mjs +15 -12
  72. package/board/tickets.mjs +3 -3
  73. package/board/worktree.mjs +21 -15
  74. package/docs/hub-session.md +4 -4
  75. package/hooks-plugin/hooks/codex-share-auto.mjs +1 -1
  76. package/hooks-plugin/hooks/watcher-stop.mjs +2 -2
  77. package/hub/board-repo.mjs +1 -1
  78. package/hub/boards.mjs +4 -4
  79. package/hub/close-board.mjs +1 -1
  80. package/hub/front-door.mjs +20 -0
  81. package/hub/github.mjs +4 -4
  82. package/hub/home.mjs +1 -1
  83. package/hub/hub.mjs +11 -14
  84. package/hub/incoming.mjs +2 -0
  85. package/hub/linear.mjs +2 -2
  86. package/hub/live.mjs +2 -2
  87. package/hub/new-board.mjs +2 -2
  88. package/hub/owner-checks.mjs +1 -0
  89. package/hub/permission-denied-hook.mjs +2 -0
  90. package/hub/pr-report.mjs +3 -1
  91. package/hub/presence.mjs +1 -1
  92. package/hub/prompts.mjs +1 -1
  93. package/hub/push.mjs +2 -2
  94. package/hub/readiness.mjs +2 -2
  95. package/hub/repos.mjs +3 -3
  96. package/hub/restart-board.mjs +1 -1
  97. package/hub/schedule.mjs +1 -1
  98. package/hub/sessions.mjs +2 -2
  99. package/hub/sw.js +1 -1
  100. package/hub/try.mjs +8 -4
  101. package/hub/usage.mjs +3 -3
  102. package/hub/views.mjs +1 -1
  103. package/package.json +5 -1
  104. package/shared/card-prs.mjs +1 -0
  105. package/shared/colors.css +5 -4
  106. package/shared/fake-gh.mjs +59 -8
  107. package/shared/owner-check.mjs +3 -0
  108. package/shared/repos.mjs +1 -1
  109. package/shared/tailscale.mjs +1 -0
  110. package/shared/usage-gate.mjs +1 -1
  111. package/board/public/view-polling.js +0 -87
package/hub/hub.mjs CHANGED
@@ -1,19 +1,16 @@
1
1
  #!/usr/bin/env node
2
- var $t=Object.defineProperty;var c=(e,t)=>$t(e,"name",{value:t,configurable:!0});import Te from"node:http";import Oe from"node:net";import{spawn as je,execFileSync as _t}from"node:child_process";import g from"node:fs";import k from"node:path";import Pe from"node:os";import{fileURLToPath as xt}from"node:url";import Re from"node:vm";import vt from"node:zlib";import{routeRecord as j}from"./routes.mjs";import{publicKey as Tt,addSubscription as Ot,removeSubscription as jt,sendAll as Pt,flushQueue as Ee,queueStatus as le,saveSettings as Rt,readSettings as A,migrateSettings as Et,setNotifications as Dt,readCodexShare as De,setCodexShare as Lt,readClaudeModel as Ct,setClaudeModel as Ht,deliver as At,pushRoute as It}from"./push.mjs";import{POLL_MS as Le,answerOf as Mt,expired as Bt,pendingFor as Ce,promptRecord as Ft,pushOf as Ut,updatePrompts as I,shown as Nt,pruneRemembered as de,rememberRules as Gt,rememberedAnswer as zt,rememberedFor as He}from"./prompts.mjs";import{nextSlot as Kt,windowAt as Wt,hold as Jt,heldUntil as Vt,holdLine as Zt,editPolling as Xt}from"./schedule.mjs";import{capital as Qt,hubView as Yt,settingsView as qt,pollingView as en,flushView as tn,SNOOZE_REFUSED as nn,AWAKE_REFUSED as an}from"./views.mjs";import{caffeinate as sn,setKeepAwake as rn,awakeUntil as Ae,awakeUnsupported as on}from"./awake.mjs";import{checkGitHub as cn,sendWakes as un}from"./github.mjs";import{listBoards as R,isBoardSite as ln,listenState as Ie,RESUME_STATE as dn,sendToBoards as Me,checkListening as hn,recordHubCode as fn,runningHubCode as pn,recordLinkFaults as mn,linksOf as gn,hubLink as wn,hubCodeState as bn,readJSON as yn}from"./boards.mjs";import{liveSessions as Be}from"./live.mjs";import{RuleError as Sn,applyRules as kn,changed as Fe,readHubRules as he,writeHubRules as $n}from"./rules.mjs";import{siteIcon as _n}from"./icons.mjs";import{readNeeds as xn,sidebarSites as vn,startRoute as Tn,hubHealth as On}from"./home.mjs";import{checkSessions as jn,startingBoards as Pn}from"./sessions.mjs";import{repoService as Rn,repoLookupRoute as En}from"./repos.mjs";import{boardRepo as Dn,changeBoardRepo as Ln,migrateBoardRepos as Cn,migrateHubBoard as Hn}from"./board-repo.mjs";import{requestBoard as An,retryBoard as In,dismissBoard as Mn}from"./new-board.mjs";import{boardClosure as Bn,closeBoard as Fn,closedBoards as Un,reopenBoard as Nn}from"./close-board.mjs";import{restartBoard as Gn,restartable as zn}from"./restart-board.mjs";import{crossSite as Kn,isJSON as fe}from"../starter/cross-site.mjs";import{BOARD_PUBLIC as pe,appFile as Wn,colorsVersion as Ue,destOf as Jn,fileVersion as Vn,pageOf as Ne,shellHtml as Zn}from"./shell.mjs";import{hubStream as Xn}from"./stream.mjs";import{usageReader as Qn,checkUsagePause as Yn,contextReading as qn}from"./usage.mjs";import{readUsageGate as Ge,setPauseAt as ea,setKeepGoing as ta}from"../shared/usage-gate.mjs";import{readiness as na,readCodex as W}from"./readiness.mjs";import{BuildError as aa,startBuild as sa,stopTries as ra,tryTick as ze,initializeTries as oa,fetchTryBranches as Ke}from"./try.mjs";import{serveFiles as ia}from"./files.mjs";import{upgrader as ca,CHECK_MS as ua}from"./upgrade.mjs";import{openBoardBuilders as la}from"../board/store.mjs";import{cloneRoot as We}from"../shared/repos.mjs";import{LinearError as me,linearStatus as J,removeLinearKey as da,saveLinearKey as ha,savedClient as Je}from"./linear.mjs";const V=k.dirname(xt(import.meta.url)),fa=k.join(V,"..","shared","colors.css"),u=c((...e)=>console.log(new Date().toISOString(),...e),"log"),Z=k.join(Pe.homedir(),".claude","kickflip"),Ve=k.join(Z,"manifest.json"),X=k.join(Z,"logs"),Ze=k.join(Z,"run"),ge=Number(process.env.KICKFLIP_HUB_PORT||4500);for(const e of[Z,X,Ze])g.mkdirSync(e,{recursive:!0});try{const e=fn();u(`hub code ${e.hash} (${e.files.length} files)`)}catch(e){u("could not record the hub code hash:",e.message)}try{const e=await Cn();e.length&&u(`manifest: recorded GitHub repos for ${e.map(t=>`/${t}`).join(", ")}`)}catch(e){u("board repo migration failed:",e.message)}try{const e=await Hn();e&&u(`manifest: /${e}, the hub board, builds from its own folder's local main`)}catch(e){u("hub board migration failed:",e.message)}const L=Rn(),pa=Qn(),Q=sn({log:u});Q.apply();let Y={version:1,sites:{}};function w(){try{Y=JSON.parse(g.readFileSync(Ve,"utf8")),Y.sites||={}}catch(e){e.code==="ENOENT"?Y={version:1,sites:{}}:u("manifest unreadable, keeping the last good copy:",e.message)}return Y}c(w,"loadManifest");const T=new Map,C=new Set;let q=!1;const Xe=5e3,ee=c(e=>JSON.stringify([e.cmd,e.cwd||e.dir,e.port,e.env||{},e.restart_at||0]),"keyOf"),we=c(e=>Array.isArray(e?.cmd)&&e.cmd.length>0&&!!e.port,"runs"),M=c(e=>we(e)?"cmd":e?.port?"port":"files","servesOf"),B=new Map;function te(e){return k.join(Ze,e+".pid")}c(te,"pidFile");function ma(e,t){try{const n=Number(g.readFileSync(te(e),"utf8")),r=n?_t("ps",["-o","command=","-p",String(n)],{encoding:"utf8"}).trim():"";r&&r.includes(t.cmd[t.cmd.length-1])&&(process.kill(-n,"SIGTERM"),u(`stopped stale ${e} (pgid ${n})`))}catch{}try{g.unlinkSync(te(e))}catch{}}c(ma,"killStale");function ga(e,t){const n=T.get(e)||{restarts:0},r=g.openSync(k.join(X,e+".log"),"a"),[a,...l]=t.cmd,h={...process.env,...t.env||{},PORT:String(t.port),PREFIX:"/"+e,KICKFLIP_ROUTE:e};g.writeSync(r,`
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,`
3
3
  --- ${new Date().toISOString()} hub starting: ${t.cmd.join(" ")} (port ${t.port})
4
- `);let i;try{i=je(a,l,{cwd:t.cwd||t.dir,env:h,stdio:["ignore",r,r],detached:!0})}catch(b){g.writeSync(r,`hub could not start it: ${b.message}
5
- `),g.closeSync(r),Object.assign(n,{child:null,key:ee(t),state:"crashed",exitCode:null,nextStart:Date.now()+Qe(n.restarts++)}),T.set(e,n);return}g.closeSync(r),g.writeFileSync(te(e),String(i.pid)),Object.assign(n,{child:i,key:ee(t),state:"starting",since:Date.now(),exitCode:null,up:!1}),T.set(e,n),u(`started ${e} pid ${i.pid}`),i.pid&&C.add(i);const y=c((b,S)=>{if(C.delete(i),n.child!==i)return;try{g.unlinkSync(te(e))}catch{}Date.now()-n.since>6e4&&(n.restarts=0),Object.assign(n,{child:null,state:n.stopping?"stopped":"crashed",exitCode:b??S,nextStart:Date.now()+Qe(n.restarts++)}),n.stopping=!1,u(`${e} exited (${b??S})`)},"ended");i.on("error",b=>{u(`${e} spawn error: ${b.message}`),i.pid||y(b.code,null)}),i.on("exit",y)}c(ga,"start");const Qe=c(e=>Math.min(6e4,1e3*2**Math.min(e,6)),"backoff");function be(e){const t=T.get(e);if(!t||!t.child){T.delete(e);return}t.stopping=!0;const n=t.child;ne(n,"SIGTERM"),setTimeout(()=>{C.has(n)&&ne(n,"SIGKILL")},Xe)}c(be,"stop");const ne=c((e,t)=>{try{process.kill(-e.pid,t)}catch{}},"signal");function Ye(e){return new Promise(t=>{const n=Oe.connect({port:e,host:"127.0.0.1"}),r=c(a=>{n.destroy(),t(a)},"done");n.setTimeout(800,()=>r(!1)),n.on("connect",()=>r(!0)),n.on("error",()=>r(!1))})}c(Ye,"probe");let qe=!0;const F=new Set,ae=new Set,wa=3e4;async function ba(e){const t=Date.now()+wa;for(;;){const n=j(w().sites,e),r=T.get(e);if(n&&r?.child&&r.up&&r.key===ee(n))return!0;if(Date.now()>=t)return!1;await new Promise(a=>setTimeout(a,250))}}c(ba,"serverAnswers");async function et(){if(q)return;const{sites:e}=w();for(const t of[...T.keys()])F.has(t)||(j(e,t)?we(j(e,t))||(u(`${t} no longer has a command; stopping it`),be(t),T.delete(t)):(u(`${t} left the manifest; stopping it`),be(t),T.delete(t)));for(const t of[...B.keys()])M(j(e,t))!=="port"&&B.delete(t);for(const[t,n]of Object.entries(e))M(n)==="port"&&B.set(t,await Ye(n.port));for(const[t,n]of Object.entries(e)){if(!we(n))continue;qe&&ma(t,n);const r=T.get(t);if(r&&r.child&&r.key!==ee(n)){u(`${t} entry changed; restarting it`),be(t),r.restarts=0,r.nextStart=Date.now()+1500;continue}(!r||!r.child&&Date.now()>=(r.nextStart||0))&&ga(t,n)}qe=!1;for(const[t,n]of T){const r=j(e,t);r&&(n.up=n.child?await Ye(r.port):!1,n.child&&n.up&&(n.state="up"))}}c(et,"reconcile");const ya=setInterval(()=>et().catch(e=>u("reconcile failed:",e.message)),2e3);et().catch(e=>u("reconcile failed:",e.message));try{Et()&&u("settings: migrated the push schedule to the polling interval")}catch(e){u("settings migration failed:",e.message)}const Sa=6e4,tt=60*6e4,$={running:null,startedAt:0,firstAt:Date.now()+Sa,last:null,errorLoggedAt:0,linearErrorLoggedAt:0},nt=c(()=>new Date($.startedAt?Kt(new Date($.startedAt),A()).getTime():$.firstAt),"nextPollAt");function at({flush:e=!0}={}){if($.running)return $.running;const t=new Date;return $.startedAt=t.getTime(),$.running=(async()=>{const n=A();let r;try{r=await cn({every:Wt(t,n).every_minutes,manifest:w()})}catch(a){r={at:t.toISOString(),watched:0,failed:[a.message],changes:[],events:[],checked:[],merged:[],linear:{moved:[],failed:[]}}}r.merged.length&&(await Ke({manifest:w(),log:u,routes:r.merged}),u(`merged on ${r.merged.join(", ")}: fetched and refreshing Main`),ze({manifest:w(),routes:r.merged}).catch(a=>u("merge refresh failed:",a.message))),await Ke({manifest:w(),log:u}),r.failed.length&&Date.now()-$.errorLoggedAt>tt&&($.errorLoggedAt=Date.now(),u(`github check: ${r.failed.length} step${r.failed.length===1?"":"s"} failed, first: ${r.failed[0]} (logged at most hourly)`)),r.changes.length&&u(`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:l}of r.linear.moved)u(`linear: ${a} \u2192 ${l}`);r.linear.failed.length&&Date.now()-$.linearErrorLoggedAt>tt&&($.linearErrorLoggedAt=Date.now(),u(`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 hn({manifest:w()})}catch(a){r.deaf={pushed:[],failed:[a.message]}}(r.deaf.pushed.length||r.deaf.failed.length)&&u(`not listening: pushed ${r.deaf.pushed.join(", ")||"none"}${r.deaf.failed.length?`; ${r.deaf.failed.join("; ")}`:""}`);try{mn(w())}catch(a){u("could not record link faults:",a.message)}if(r.flushed=null,r.held=Vt(t,n)!==null,e&&!r.held)try{const a=await Ee();a&&(r.flushed={count:a.count,sent:a.sent,ok:a.ok},u(`push queue: sent ${a.sent} of ${a.count} queued`))}catch(a){u("push queue flush failed:",a.message)}return $.last=r,r})().finally(()=>{$.running=null}),$.running}c(at,"runPoll");let st=null;function ka(){st||=un({settings:A(),manifest:w()}).then(e=>{e.due.length&&u(`wake ${e.due.join(", ")}: check-github \u2192 ${e.events.map(t=>t.route).join(", ")||"no board"}${e.failed.length?`; ${e.failed.join("; ")}`:""}`)}).catch(e=>u("wake failed:",e.message)).finally(()=>{st=null})}c(ka,"runWakes");const ye=new Set,D={running:null};function rt(){if(D.running||F.size||ae.size)return;const e=new Set(ye);ye.clear(),D.running=jn({manifest:w(),asked:e}).then(t=>{(t.resumed.length||t.pushed.length||t.failed.length)&&u(`sessions: resumed ${t.resumed.join(", ")||"none"}; pushed ${t.pushed.join(", ")||"none"}${t.failed.length?`; ${t.failed.join("; ")}`:""}`)}).catch(t=>u("session check failed:",t.message)).finally(()=>{D.running=null})}c(rt,"runSessionCheck"),await oa(w());const $a=Date.now(),U=new Map;function N(e){e?(se.delete(e),_.changed(e)):_.refresh()}c(N,"promptChanged");async function ot(){const{gone:e,rulesChanged:t}=await I(n=>{const r=Bt(n,{now:Date.now(),aliveSince:$a}),a=de(n,Se());for(const l of r)delete n.prompts[l.id];return{gone:r,rulesChanged:a}});for(const n of e)N(n.route);t&&_.refresh(),e.length&&u(`prompts: dropped ${e.map(n=>n.id).join(", ")}, whose hook went away`)}c(ot,"prunePrompts");const Se=c(()=>({running:Be(),boards:R(w())}),"promptContext");let G=null,it=null;async function ke(){return G?it??=G.catch(()=>{}).then(()=>(it=null,ke())):(G=(async()=>Yn({gate:Ge(),resume:c(async e=>{const t=await Me(R(w()),"usage-resumed",{window:e});for(const n of t.failed)u(n)},"resume")}))().finally(()=>{G=null}),G)}c(ke,"checkUsageGate"),setInterval(()=>{ze({manifest:w()}).catch(e=>u("try tick failed:",e.message)),z.tick().catch(e=>u("kickflip update tick failed:",e.message)),ke().catch(e=>u("usage gate failed:",e.message)),!$.running&&Date.now()>=nt().getTime()&&at().catch(e=>u("poll failed:",e.message)),ka(),Q.apply(),ot().catch(e=>u("prompt expiry failed:",e.message)).finally(rt)},15e3);function ct(){if(q)return;q=!0,clearInterval(ya),Q.stop(),u("hub stopping; stopping sites");for(const n of T.values())n.child&&(n.stopping=!0);for(const n of C)ne(n,"SIGTERM");let e=!1;ra().finally(()=>{e=!0});const t=Date.now()+Xe;setInterval(()=>{if(!((!e||C.size)&&Date.now()<t)){for(const n of C)u(`pid ${n.pid} ignored SIGTERM; killing it`),ne(n,"SIGKILL");process.exit(0)}},50)}c(ct,"shutdown"),process.on("SIGTERM",ct),process.on("SIGINT",ct);const ut=new Map;function lt(e,t={n:3e3}){let n=0,r;try{r=g.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 l=k.join(e,a.name);if(a.isDirectory())n=Math.max(n,lt(l,t));else try{n=Math.max(n,g.statSync(l).mtimeMs)}catch{}}return n}c(lt,"newestMtime");function _a(e,t){const n=ut.get(e);if(n&&Date.now()-n.checked<3e4)return n.at;const r=lt(t.dir);return ut.set(e,{at:r,checked:Date.now()}),r}c(_a,"changedAt");const dt=c(e=>{try{return typeof e=="string"&&g.statSync(e).isDirectory()}catch{return!1}},"isFolder");function $e(){const{sites:e}=w();return Object.entries(e).sort(([t],[n])=>t.localeCompare(n)).map(([t,n])=>{const r=T.get(t)||{},a=M(n),l=a==="files"?dt(n.dir)?"up":"down":a==="port"?B.has(t)?B.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:l,exitCode:r.exitCode??null,owner:n.owner||null,restart:zn(n),changedAt:_a(t,n)||null,created:n.created||null,log:k.join(X,t+".log")}})}c($e,"status");const H=c(e=>String(e??"").replace(/[&<>"']/g,t=>({"&":"&amp;","<":"&lt;",">":"&gt;",'"':"&quot;","'":"&#39;"})[t]),"esc"),se=new Map,ht=2e4,xa=1500;async function va(e){try{const t=await fetch(`http://127.0.0.1:${e}/_claude/needs`,{signal:AbortSignal.timeout(xa)});return t.ok?xn(await t.json()):null}catch{return null}}c(va,"fetchNeeds");async function ft(e,{fresh:t=!1}={}){const n=e.filter(a=>a.state==="up"&&a.owner&&a.port);for(const a of n){const l=se.get(a.route);(t===!0||t?.has?.(a.route)||!l||Date.now()-l.at>=ht)&&se.set(a.route,{at:Date.now(),needs:va(a.port)})}const r=await Promise.all(n.map(a=>se.get(a.route).needs));return new Map(n.map((a,l)=>[a.route,r[l]]))}c(ft,"waitingCounts");const pt=c(e=>[...e.values()].reduce((t,n)=>t+(n?.count||0),0)+Ce(null).length,"badgeTotal"),_e=new Map;function Ta(e){const t=new Map,n=new Map,r={running:Be(),wakes:yn(dn,{})};for(const a of e)try{const l=Ie(a,r);n.set(a.route,l);let h=_e.get(a.route);(!h||h.listen!==l||Date.now()-h.at>=ht)&&(h={at:Date.now(),listen:l,links:gn(a,{listen:l})},_e.set(a.route,h)),t.set(a.route,h.links)}catch(l){u(`links of ${a.route} failed:`,l.message),t.set(a.route,[])}return{links:t,listens:n}}c(Ta,"boardLinks");async function re({fresh:e=!1}={}){const t=new Date,n=$e(),r=await ft(n,{fresh:e}),a=w(),l=R(a),{links:h,listens:i}=Ta(l),y=bn(),b=new Map(l.map(s=>[s.route,qn(s.owner?.session)]).filter(([,s])=>s)),S=vn(n,r,new Set(l.map(s=>s.route)),i,b),m=A(),p=Jt(t,m);return{data:{code:y.running??null,total:pt(r),start:Tn(S),sites:S,hub:On(S,h,wn(y)),home:g.realpathSync(Pe.homedir()),starting:Pn(a,t.getTime()),usage:pa({codexShare:De(W())}),pause:Ge(),upgrade:z.state(),notifications:p?p.why==="off"?{state:"off",text:"Off"}:{state:"held",text:Qt(Zt(p,t))}:{state:"on",text:"On"}},links:h,rows:n,code:y}}c(re,"sidebarState");function mt(e,t,n,r,a){let l=Buffer.isBuffer(a)?a:Buffer.from(a);const h={...r,vary:[r.vary,"accept-encoding"].filter(Boolean).join(", ")};l.length>=1024&&/\bgzip\b/.test(e.headers["accept-encoding"]||"")&&(l=vt.gzipSync(l),h["content-encoding"]="gzip"),h["content-length"]=l.length,t.writeHead(n,h),t.end(e.method==="HEAD"?void 0:l)}c(mt,"sendText");async function Oa(e){const{data:t}=await re(),n=Re.createContext({});for(const r of["ui-page-header.js","ui-sidebar.js"])Re.runInContext(g.readFileSync(k.join(pe,r),"utf8"),n);return Zn({sidebar:n.BoardUI.sidebar(t,{here:e}),sidebarData:JSON.stringify(t).replace(/</g,"\\u003c"),touchIcon:oe(180)})}c(Oa,"shellPage");const z=ca({root:We(),builders:c(async()=>(await Promise.all(R(w()).map(e=>la(e.dir)))).flat(),"builders"),run:ja,log:u,changed:c(()=>_.refresh(),"changed")});function ja(){const e=k.join(X,"kickflip-update.log");g.writeFileSync(e,`${new Date().toISOString()} kickflip update
6
- `);const t=g.openSync(e,"a");return u("running kickflip update"),new Promise(n=>{const r=je(process.execPath,[k.join(We(),"bin","kickflip.mjs"),"update"],{detached:!0,stdio:["ignore",t,t]});g.closeSync(t),r.unref(),r.on("error",a=>n({ok:!1,error:a.message})),r.on("exit",a=>{const l=g.readFileSync(e,"utf8").trim().split(`
7
- `).at(-1);u(`kickflip update exited ${a}: ${l}`),n(a===0?{ok:!0}:{ok:!1,error:l.replace(/^kickflip: /,"")})})})}c(ja,"runUpdate"),z.check(),setInterval(()=>z.check(),ua);const _=Xn({targets:c(()=>Object.entries(w().sites).filter(([,e])=>ln(e)&&e.port).map(([e,t])=>({route:e,port:t.port})),"targets"),watched:c(()=>R(w()).map(({route:e,dir:t})=>({route:e,dir:t})),"watched"),sidebar:c(async({fresh:e})=>(await re({fresh:e})).data,"sidebar"),log:u});function oe(e){let t="0";try{t=Math.floor(g.statSync(k.join(V,`icon-${e}.png`)).mtimeMs).toString(36)}catch{}return`/_hub/icon-${e}.png?v=${t}`}c(oe,"iconUrl");const Pa=c(()=>JSON.stringify({name:"Claude sites",short_name:"Claude",start_url:"/",scope:"/",display:"standalone",background_color:"#121514",theme_color:"#121514",icons:[{src:oe(192),sizes:"192x192",type:"image/png"},{src:oe(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}c(E,"readBody");async function O(e,t){if(!fe(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):{}}c(O,"readJSONBody");const gt=c((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>${H(e)}</title><link rel="manifest" href="/manifest.webmanifest"><link rel="icon" type="image/png" href="/_hub/kickflip.png"><link rel="apple-touch-icon" href="${oe(180)}">
9
- <meta name="apple-mobile-web-app-title" content="Claude">${t}<link rel="stylesheet" href="/_hub/colors.css?v=${Ue()}"><link rel="stylesheet" href="/_hub/hub.css"></head><body><main>`,"head"),Ra=c(e=>/\bapplication\/json\b/.test(e.headers.accept||""),"wantsJSON"),wt=c(()=>({running:!!$.running,nextAt:nt(),last:$.last}),"pollState");async function ie(e){await ot();const t=new Date,{data:n,links:r,rows:a,code:l}=await re();return{...Yt({now:t,st:le(t),poll:wt(),sidebar:n,rows:a,links:r,rules:he(),integrations:await na.rows(),code:l,host:e.headers["x-forwarded-host"]||e.headers.host||"",codexShare:De(W()),claudeModel:Ct(),codex:W(),awake:Ae(t),remembered:new Map(R(w()).map(h=>[h.route,He(h.route)]))}),prompts:Ce(null).map(Nt),remembered:He(null)}}c(ie,"hubData");const ce=c(async()=>{const e=new Date,t={until:Ae(e),unsupported:on()};return{...qt({now:e,st:le(e),rules:he(),route:await It(),linear:J(),awake:t}),closed:Un()}},"settingsData"),bt=c(e=>{const t=new Date;return en({now:t,st:le(t),poll:wt(),select:e})},"pollingData"),P=c((e,t)=>{e.writeHead(303,{location:t,"cache-control":"no-store"}),e.end()},"redirect");async function Ea(e,t,n){const r=c((s,o,d,f={})=>{t.writeHead(s,{"content-type":o,"cache-control":"no-store",...f}),t.end(d)},"send"),a=c((s,o)=>{const d=pn();return r(s,"application/json",JSON.stringify(o),d?{"x-hub-code":d}:{})},"sendJSON"),l=c(({route:s,started:o})=>(o.then(({error:d})=>{d&&u(`new board /${s} didn't start: ${d}`),_.refresh()}),_.refresh(),a(200,{route:s})),"starting"),h=Ra(e),i=n.pathname,y=/^\/_hub\/close-board\/([a-z0-9-]+)$/.exec(i);if(y&&e.method==="GET")try{return a(200,await Bn(y[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 O(e);if(s=o.route,i==="/_hub/reopen-board"){const v=await Nn(s);return _.refresh(),a(200,v)}if(F.has(s))return a(409,{error:"That board is already closing."});F.add(s);const d=c(()=>F.delete(s),"release");t.once("finish",d),t.once("close",d),await D.running;const f=await Fn(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 O(e)).route,ae.has(s))return a(409,{error:"That board is already restarting."});ae.add(s),await D.running;const o=await Gn(s,{serverUp:ba});return u(`restarted /${s}: server answers, session ${o.session} resumed`),a(200,o)}catch(o){return s&&u(`restart /${s} failed: ${o.message}`),a(o.status||400,{error:o.message})}finally{ae.delete(s),_.refresh()}}if(i==="/_hub/upgrade"&&e.method==="POST")try{return await O(e),a(200,{upgrade:await z.request()})}catch(s){return a(s.status||400,{error:s.message})}if(i==="/_hub/wake"&&e.method==="POST")try{const{route:s}=await O(e),o=R(w()).find(d=>d.route===s);return o?(ye.add(s),await D.running,rt(),await D.running,_e.clear(),_.changed(s),a(200,{route:s,listen:Ie(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(w(),{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 En(n,{repos:L,manifest:w(),sendJSON:a});const b=/^\/_hub\/board-repo\/([a-z0-9-]+)$/.exec(i);if(b&&["GET","POST"].includes(e.method))try{const s=e.method==="GET"?Dn(b[1]):await Ln(b[1],await O(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(!fe(e.headers))return r(403,"text/plain; charset=utf-8","Send name, github, branch and purpose as application/json.");let s;try{s=await O(e)}catch{return r(400,"text/plain; charset=utf-8","Send name, github, branch and purpose as JSON.")}try{return l(await An(s,{repos:L,changed:c(()=>_.refresh(),"changed")}))}catch(o){if(o.status)return r(o.status,"text/plain; charset=utf-8",o.message);throw o}}const S=/^\/_hub\/new-board\/([a-z0-9-]+)\/retry$/.exec(i);if(S&&e.method==="POST"){if(!fe(e.headers))return r(403,"text/plain; charset=utf-8","Send the retry as application/json.");try{return l(await In(S[1],{repos:L,changed:c(()=>_.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 Mn(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"&&e.method==="POST"){if(q)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:"Builds can only be started from the host over loopback."});let d;try{d=await O(e)}catch(f){return a(f.status||400,{error:"Send the board route and card key as JSON."})}try{return a(202,await sa(d,{manifest:w(),log:u}))}catch(f){if(f instanceof aa)return a(f.status,{error:f.message});throw f}}if(i==="/sw.js")return r(200,"text/javascript; charset=utf-8",g.readFileSync(k.join(V,"sw.js")),{"service-worker-allowed":"/"});if(i==="/manifest.webmanifest")return r(200,"application/manifest+json",Pa());const p=/^\/_hub\/icon-(180|192|512)\.png$/.exec(i);if(p)try{return r(200,"image/png",g.readFileSync(k.join(V,`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")===Ue()?"public, max-age=31536000, immutable":"no-cache";try{return r(200,"text/css; charset=utf-8",g.readFileSync(fa),{"cache-control":s})}catch{t.writeHead(404).end();return}}if(i==="/_hub/kickflip.png")try{return r(200,"image/png",g.readFileSync(k.join(pe,"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",g.readFileSync(k.join(pe,"hub.css")));if(i==="/_hub/sidebar.json")return a(200,(await re({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=Wn(i.slice(10));if(!s){t.writeHead(404).end();return}const o=k.extname(s),d={".js":"text/javascript; charset=utf-8",".css":"text/css; charset=utf-8",".png":"image/png"}[o],f=n.searchParams.get("v")===Vn(s)?"public, max-age=31536000, immutable":"no-cache";return o===".png"?r(200,d,g.readFileSync(s),{"cache-control":f}):mt(e,t,200,{"content-type":d,"cache-control":f},g.readFileSync(s))}if(i==="/_hub/api/hub")return a(200,await ie(e));if(i==="/_hub/api/settings")return a(200,await ce());if(i==="/_hub/api/polling")return a(200,bt(n.searchParams.get("w")));if(i==="/_hub/push/key")return a(200,{key:Tt()});if(i==="/_hub/push/subscribe"&&e.method==="POST"){const s=e.headers["x-forwarded-host"]||e.headers.host;return a(200,{count:Ot(await O(e),`https://${s}/`,e.headers["user-agent"])})}if(i==="/_hub/push/unsubscribe"&&e.method==="POST")return a(200,{count:jt((await O(e))?.endpoint)});if(i==="/_hub/push/test"&&e.method==="POST"){const s=await Pt({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"),d=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(!d)return h?a(400,{error:f}):r(400,"text/plain; charset=utf-8",f);let v;try{v=await Dt(d)}catch(K){if(K.status!==400)throw K;return h?a(400,{error:nn}):P(t,"/_hub/?refused=snooze#notifications")}return v.dropped&&u(`notifications off: dropped ${v.dropped} queued push${v.dropped===1?"":"es"}`),h?a(200,await ce()):P(t,"/_hub/#notifications")}if(i==="/_hub/settings/keep-awake"&&e.method==="POST"){const s=new URLSearchParams(await E(e));try{rn(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:an}):P(t,"/_hub/?refused=keep-awake#keep-awake")}return Q.apply(),h?a(200,await ce()):P(t,"/_hub/#keep-awake")}if(i==="/_hub/settings/codex"&&e.method==="POST"){const s=new URLSearchParams(await E(e));try{Lt(s.get("value"),W({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 ie(e)):P(t,"/_hub/")}if(i==="/_hub/settings/claude-model"&&e.method==="POST"){const s=new URLSearchParams(await E(e));try{Ht(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 ie(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")?ea(s.get("value")):ta(s.get("value")),await ke()}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 ie(e)):P(t,"/_hub/")}if(i==="/_hub/settings/linear"&&e.method==="POST"){const s=new URLSearchParams(await E(e,4096));try{s.has("remove")?(da(),u("linear: key removed")):u(`linear: key ${(await ha(s.get("key"))).refused?"refused by Linear":"saved"}`)}catch(o){if(!(o instanceof me))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 ce()):P(t,"/_hub/#linear")}if(i==="/_hub/api/linear"&&e.method==="GET"){const s=J();if(!s.set||s.refused)return a(200,s);const o=Je();try{const d=await o.teams();return a(200,{...J(),teams:d.map(({key:f,name:v})=>({key:f,name:v})),projects:d.flatMap(f=>f.projects.map(v=>({id:v.id,name:v.name,team:f.key})))})}catch(d){if(!(d instanceof me))throw d;return a(200,{...J(),error:d.message})}}if(i==="/_hub/linear/projects"&&e.method==="POST"){const s=Je();if(!s)return a(400,{error:"Add a Linear key on Hub first."});let o;try{o=await O(e,4*1024)}catch(f){return a(f.status||400,{error:"Send the team and project name as JSON."})}const d=typeof o.name=="string"?o.name.trim():"";if(!d||d.length>80||typeof o.team!="string"||!/^[A-Z][A-Z0-9]*$/.test(o.team))return a(400,{error:"Name the project and its team."});try{return a(200,await s.createProject(o.team,d))}catch(f){if(!(f instanceof me))throw f;return a(f.status,{error:f.message})}}if(i==="/_hub/settings/polling"&&e.method==="POST"){const s=new URLSearchParams(await E(e)),o=s.get("op")||"",d=Xt(A(),{op:o,w:s.get("w")||"",value:(s.get("value")||"").trim()});try{if(d.error)throw Object.assign(new Error(d.error),{status:400});Rt(d.input)}catch(f){if(f.status!==400)throw f;return h?a(400,{error:f.message}):r(400,"text/plain; charset=utf-8",f.message)}return h?a(200,bt(d.select)):P(t,`/_hub/?w=${encodeURIComponent(d.select)}#polling`)}if(i==="/_hub/settings/rules"&&e.method==="POST"){let s,o;try{s=await O(e,64*1024),o=kn(he(),s.rules,s.base??[]),Fe(o.changes)&&$n(o.rules)}catch(f){if(!(f instanceof Sn)&&!(f instanceof SyntaxError))throw f;return a(400,{error:f.message})}if(!Fe(o.changes))return a(200,{rules:o.rules,changed:!1,events:[],failed:[]});const d=await Me(R(w()),"rules",{scope:"hub",...o.changes});return u(`hub rules saved: rules \u2192 ${d.events.map(f=>f.route).join(", ")||"no board"}${d.failed.length?`; ${d.failed.join("; ")}`:""}`),a(200,{rules:o.rules,changed:!0,...d})}if(i==="/_hub/push/flush"&&e.method==="POST"){const s=await Ee();return s&&u(`push queue: ${s.ok?"sent":"could not send"} ${s.count} queued (from the settings page)`),h?a(200,tn(s)):P(t,`/_hub/settings/polling?flushed=${s?s.ok?s.count:"failed":0}`)}if(i==="/_hub/poll"&&e.method==="POST"){const s=await O(e);return a(200,await at({flush:s.flush!==!1}))}if(i==="/_hub/prompts"&&e.method==="POST"){let s;try{s=await O(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:d}=await I(f=>{const v=Se(),K=v.boards.find(ve=>ve.owner?.session&&ve.owner.session===s.session_id)||null;de(f,v)&&_.refresh();const xe=zt(f,s);if(xe)return{answer:xe};const ue=Ft(s,{board:K,live:v.running.get(s.session_id)});return f.prompts[ue.id]=ue,{rec:ue}});return o?a(200,{state:"answered",answer:o}):(N(d.route),u(`prompt ${d.id}: ${d.route?`/${d.route}`:d.cwd} ${d.tool}${d.action?` (${d.action})`:""}`),At(Ut(d),{kind:"prompt",immediate:!0}).catch(f=>u(`prompt ${d.id}: push failed: ${f.message}`)),a(200,{id:d.id,deadline:d.deadline}))}const x=/^\/_hub\/prompts\/([\w-]+)(\/wait|\/answer)?$/.exec(i);return x?Da(e,t,{id:x[1],op:x[2]?.slice(1)||null,url:n,sendJSON:a}):!1}c(Ea,"hubRoute");async function Da(e,t,{id:n,op:r,url:a,sendJSON:l}){if(r==="wait"&&e.method==="GET"){const h=c(()=>I(m=>{const p=j(m.prompts,n);return p?.answer?delete m.prompts[n]:p&&(p.seen=new Date().toISOString()),p}),"seen"),i=c(m=>(N(m.route),l(200,{state:"answered",answer:m.answer})),"take"),y=await h();if(!y)return l(404,{state:"gone"});if(y.answer)return i(y);const b=Math.max(0,Math.min(Number(a.searchParams.get("ms"))||Le,Le));if(await new Promise(m=>{const p=U.get(n)||new Set;U.set(n,p);const x=c(()=>{clearTimeout(s),p.delete(x),!p.size&&U.get(n)===p&&U.delete(n),m()},"done"),s=setTimeout(x,b);p.add(x),t.on("close",x)}),t.writableEnded||t.destroyed)return;const S=await h();return S?S.answer?i(S):l(200,{state:"pending"}):l(404,{state:"gone"})}if(r==="answer"&&e.method==="POST"){const h=await O(e),i=await I(S=>{const m=j(S.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:x}=Mt(m,h);if(x)return{status:400,error:x};m.answer=p;const s=Se();return de(S,s),Gt(S,m,s),{rec:m,answer:p}});if(i.error)return l(i.status,{error:i.error});const{rec:y,answer:b}=i;u(`prompt ${n}: ${b.choice}`);for(const S of[...U.get(n)||[]])S();return N(y.route),l(200,{ok:!0})}if(r===null&&e.method==="DELETE"){const h=await I(i=>{const y=j(i.prompts,n);return delete i.prompts[n],y});return h&&(N(h.route),u(`prompt ${n}: its hook gave up`)),l(200,{ok:!0})}return!1}c(Da,"promptRoute");const La=c(({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 yt(e,t){const n=T.get(e)||{},r=n.exitCode!==void 0&&n.exitCode!==null?` (last exit: ${n.exitCode})`:"",a=M(t),[l,h]=a==="port"?["down",`down \xB7 nothing answers on port ${t.port}`]:a==="files"?["down","down \xB7 its folder is missing"]:n.child?["wait","started \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`${gt(`${t.title||e} is down`,'<meta http-equiv="refresh" content="5">')}
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">')}
10
10
  <a class="back" href="/_hub/">\u2039 Hub</a>
11
- <h1>${H(t.title||e)} isn't answering</h1>
12
- <p class="lede">${H(i)}</p>
11
+ <h1>${I(t.title||e)} isn't answering</h1>
12
+ <p class="lede">${I(i)}</p>
13
13
  <h2 class="label">Site</h2>
14
- <div class="group">${La({icon:_n(e,t),title:H(t.title||e),sub:`<span class="st ${l}">${H(h)}</span>`})}</div>
14
+ <div class="group">${es({icon:Ln(e,t),title:I(t.title||e),sub:`<span class="st ${d}">${I(h)}</span>`})}</div>
15
15
  <p class="note">This page retries every 5 seconds.</p>
16
- </main></body></html>`}c(yt,"downPage");const St=c(e=>new URL(e.url.replace(/^\/+/,"/"),"http://hub"),"requestURL"),kt=Te.createServer((e,t)=>{const n=St(e);if(e.method!=="GET"&&e.method!=="HEAD"&&Kn(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(w().sites));if(Ne({method:e.method,pathname:n.pathname,search:n.search,dest:Jn(e.headers),sites:r})==="shell"){const m=n.pathname==="/"?"":n.pathname.startsWith("/_hub/")?n.pathname.slice(1):decodeURIComponent(n.pathname.split("/")[1]);Oa(m).then(p=>mt(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:ge,pid:process.pid},sites:$e()},null,2));return}if(n.pathname==="/_hub/needs.json"){const m=$e();ft(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:pt(p),sites:Object.fromEntries(m.map(x=>[x.route,p.get(x.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/")){Ea(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],l=j(w().sites,a);if(!l){t.writeHead(404,{"content-type":"text/html; charset=utf-8"}),t.end(`${gt("No Site")}<a class="back" href="/_hub/">\u2039 Hub</a><h1>No site at /${H(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)||"/",y=Ne({pathname:n.pathname,dest:"document",sites:r})==="shell";if(M(l)==="files"){if(!dt(l.dir)){t.writeHead(502,{"content-type":"text/html; charset=utf-8","cache-control":"no-store"}),t.end(yt(a,l));return}ia(e,t,{dir:l.dir,rest:i,prefix:h,headers:y?{vary:"sec-fetch-dest"}:{}}).catch(m=>{t.headersSent||t.writeHead(500,{"content-type":"text/plain"}),t.end(String(m.message||m))});return}const b={...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=Te.request({host:"127.0.0.1",port:l.port,method:e.method,path:i,headers:b},m=>{const p={...m.headers};y&&(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||S.destroy()}),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(yt(a,l))}),e.pipe(S)});kt.on("upgrade",(e,t,n)=>{const r=St(e).pathname.split("/")[1],a=j(w().sites,r);if(!a?.port){t.destroy();return}const l=`/${r}`,h=e.url.slice(l.length)||"/",i=Oe.connect(a.port,"127.0.0.1",()=>{const y=[`${e.method} ${h} HTTP/${e.httpVersion}`];for(let b=0;b<e.rawHeaders.length;b+=2)e.rawHeaders[b].toLowerCase()!=="x-forwarded-prefix"&&y.push(`${e.rawHeaders[b]}: ${e.rawHeaders[b+1]}`);y.push(`X-Forwarded-Prefix: ${l}`),i.write(y.join(`\r
17
- `)+`\r
18
- \r
19
- `),n&&n.length&&i.write(n),i.pipe(t),t.pipe(i)});i.on("error",()=>t.destroy()),t.on("error",()=>i.destroy()),t.on("close",()=>i.destroy()),i.on("close",()=>t.destroy())}),kt.listen(ge,"127.0.0.1",()=>u(`hub on http://127.0.0.1:${ge}/ (manifest ${Ve})`));
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})`));
@@ -0,0 +1,2 @@
1
+ var N=Object.defineProperty;var c=(n,r)=>N(n,"name",{value:r,configurable:!0});import g from"node:fs";import S from"node:path";import{INCOMING_FILES as y,listBoards as v,readJSON as F,saveJSON as L}from"./boards.mjs";import{openPRs as O,prFiles as R,githubLogin as C}from"./pr-report.mjs";import{cardPRs as E}from"../shared/card-prs.mjs";const k=c((n,r)=>String(n).toLowerCase()===String(r).toLowerCase(),"same");function J({open:n,cards:r,me:i}){const s=new Set(r.map(t=>t.pr));return n.filter(t=>!t.draft&&!k(t.author,i)&&!s.has(t.number)).map(t=>{const u=new Set(t.files),f=[...new Set(r.filter(o=>o.files.some(m=>u.has(m))).map(o=>o.key))],e=t.requested.some(o=>k(o,i));return f.length||e?{number:t.number,title:t.title,author:t.author,url:t.url,head:t.head,touches:f,asked:e}:null}).filter(Boolean)}c(J,"incomingFor");function M(n,r){const i=S.join(n,"data","incoming.json"),s=`${i}.${process.pid}.tmp`;g.mkdirSync(S.dirname(i),{recursive:!0}),g.writeFileSync(s,JSON.stringify(r,null,2)+`
2
+ `),g.renameSync(s,i)}c(M,"writeIncoming");async function _({manifest:n,gh:r={openPRs:O,prFiles:R},login:i=C}){const s=F(y,{}),t=[],u=[];let f;for(const e of v(n).filter(o=>o.auto_review&&o.github))try{const o=e.githubLogin||(f??=await i()),m=await r.openPRs(e.github),$=new Map(m.map(l=>[l.number,l])),d=[];for(const{slug:l,wave:I}of e.waves)for(const b of I.items.filter(a=>a?.status!=="dropped"))for(const{number:a}of E(b,e.github).filter(h=>h.repo===e.github)){const h=`${e.github}#${a}`;let p=$.get(a)?.files||s[h];if(!p){const w=await r.prFiles(e.github,a);p=w.files,w.state==="MERGED"&&(s[h]=p)}d.push({key:`${l}/${b.id}`,pr:a,files:p})}M(e.dir,{at:new Date().toISOString(),repo:e.github,prs:J({open:m,cards:d,me:o})}),t.push(e.route)}catch(o){u.push(`${e.route}: ${o.message}`)}return L(y,s),{checked:t,failed:u}}c(_,"checkIncoming");export{_ as checkIncoming,J as incomingFor};
package/hub/linear.mjs CHANGED
@@ -1,2 +1,2 @@
1
- var j=Object.defineProperty;var a=(s,e)=>j(s,"name",{value:e,configurable:!0});import f from"node:fs";import $ from"node:path";import C from"node:os";class l extends Error{static{a(this,"LinearError")}constructor(e,r=502){super(e),this.status=r}}const k=a((s=C.homedir())=>$.join(s,".claude","kickflip","linear.json"),"keyFile"),_=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 v(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(v,"linearStatus");function S(s){const e=k();f.mkdirSync($.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(S,"writeKeyFile");function I(s,{fetch:e=globalThis.fetch,checked:r=a(()=>{},"checked")}={}){async function u(n,t={}){let i;try{i=await e(_(),{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 o=await i.json().catch(()=>null);if(i.status===401||o?.errors?.some(c=>c.extensions?.type==="authentication error"))throw r(!1),new l("Linear refused the key.",400);if(o?.errors?.length){const c=o.errors[0];throw Object.assign(new l(`Linear: ${c.message}`),{notFound:c.extensions?.type==="not found"||/not found/i.test(c.message)})}if(!i.ok||!o?.data)throw new l(`Linear answered ${i.status}.`);return r(!0),o.data}a(u,"call");const p="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 { ${p} } } }`,{key:n})).teams.nodes[0]||(()=>{throw new l(`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 l(`Linear's ${n.key} team has no project ${t}.`,400)})(),"projectOf"),y={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 l("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),w={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:w});if(!h.issueCreate.success)throw new l("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 y.issue(n);if(!c||c.assignee!==i&&!o||x(c.state)>={started:1,review:2,done:3}[t])return null;const w=L((await d(c.team)).states.nodes,t);if(!w)return null;if(!(await u("mutation($id: String!, $stateId: String!) { issueUpdate(id: $id, input: { stateId: $stateId }) { success } }",{id:n,stateId:w.id})).issueUpdate.success)throw new l("Linear did not move the issue.");return w.name},async moveIssue(n,t){const i=await y.issue(n);if(!i)throw new l(`Linear has no issue ${n}.`,400);if(["completed","canceled"].includes(i.state.type))return null;const o=L((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 l("Linear did not move the issue.");return o.name}};return y}a(I,"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 L(s,e){const r=[...s].sort((d,m)=>d.position-m.position),u=a(d=>r.filter(m=>m.type===d),"of"),p=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=>!p(d))||null:e==="review"?u("started").find(p)||null:e==="done"&&(u("completed").find(d=>/^done$/i.test(d.name))||u("completed")[0])||null}a(L,"stateFor");async function U(s,{fetch:e}={}){const r=typeof s=="string"?s.trim():"";if(!r||/\s/.test(r)||r.length>200)throw new l("Paste a Linear personal API key.",400);const u=new Date().toISOString();let p=!1;const d=await I(r,{fetch:e,checked:a(m=>{p=!m},"checked")}).viewer().catch(m=>{if(!p)throw m;return null});return S(d?{key:r,...d,saved_at:u}:{key:r,saved_at:u,refused_at:u}),v()}a(U,"saveLinearKey");function F(s,e){const r=g();if(r?.key!==s||!!r.refused_at==!e)return;const{refused_at:u,...p}=r;S(e?p:{...p,refused_at:new Date().toISOString()})}a(F,"markChecked");function b(){return f.rmSync(k(),{force:!0}),v()}a(b,"removeLinearKey");function z({fetch:s}={}){const e=g();return e?{...I(e.key,{fetch:s,checked:a(r=>F(e.key,r),"checked")}),saved:e}:null}a(z,"savedClient");export{l as LinearError,I as linearClient,v as linearStatus,g as readLinearKey,b as removeLinearKey,U as saveLinearKey,z as savedClient,x as stageOf,L as stateFor};
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};
package/hub/live.mjs CHANGED
@@ -1,2 +1,2 @@
1
- var m=Object.defineProperty;var s=(t,n)=>m(t,"name",{value:n,configurable:!0});import p from"node:fs";import c from"node:path";import d from"node:os";import{execFileSync as l}from"node:child_process";const a=c.join(d.homedir(),".claude","sessions"),f=2e3,o=s(t=>Date.parse(`${String(t).trim().replace(/\s+/g," ")} UTC`),"utc");function S(t,n){return t.filter(e=>{if(!n.has(e.pid))return!1;if(typeof e.procStart!="string")return!0;const r=o(e.procStart);return Number.isNaN(r)||Math.abs(r-n.get(e.pid))<=f})}s(S,"liveOf");function g(t=a){let n=[];try{n=p.readdirSync(t).filter(e=>e.endsWith(".json"))}catch{}return n.flatMap(e=>{try{const r=JSON.parse(p.readFileSync(c.join(t,e),"utf8"));return Number.isInteger(r?.pid)&&typeof r.sessionId=="string"?[r]:[]}catch{return[]}})}s(g,"sessionRecords");function u(t){const n=new Map;if(!t.length)return n;let e="";try{e=l("ps",["-o","pid=,lstart=","-p",t.join(",")],{encoding:"utf8",timeout:3e3,env:{...process.env,TZ:"UTC"}})}catch(r){e=r.stdout||""}for(const r of e.split(`
2
- `)){const i=r.trim().match(/^(\d+)\s+(.+)$/);i&&n.set(Number(i[1]),o(i[2]))}return n}s(u,"processStarts");function I(t,n=u){if(!Number.isInteger(t?.pid)||t.pid<=0||!t.sessionId||typeof t.procStart!="string")return!1;const e=o(t.procStart);if(!Number.isFinite(e))return!1;const r=n([t.pid]).get(t.pid);return Number.isFinite(r)&&Math.abs(e-r)<=f}s(I,"verifiedProcess");function v({dir:t=a,starts:n=u}={}){const e=g(t);return new Map(S(e,n([...new Set(e.map(r=>r.pid))])).map(r=>[r.sessionId,r]))}s(v,"liveSessions");export{a as SESSIONS_DIR,S as liveOf,v as liveSessions,u as processStarts,g as sessionRecords,I 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 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};
package/hub/new-board.mjs CHANGED
@@ -1,2 +1,2 @@
1
- var B=Object.defineProperty;var o=(e,t)=>B(e,"name",{value:t,configurable:!0});import{ticketSource as A}from"../board/tickets.mjs";import{readManifest as h,withManifestLock as b,writeManifest as w}from"./boards.mjs";import{ROUTE_RE as E,routeFromName as I,routeRecord as u}from"./routes.mjs";import{repoService as O,validateGithub as L,validateBranch as P,validateRemote as _}from"./repos.mjs";import{startFailed as C,startSession as F,stopSession as G}from"./sessions.mjs";import{savedClient as M}from"./linear.mjs";const d=o((e,t=400)=>{throw Object.assign(new Error(e),{status:t})},"refuse");function T(e){(!e||typeof e!="object"||Array.isArray(e)||Object.keys(e).some(i=>!["name","github","remote","branch","purpose","tickets"].includes(i)))&&d("Send only name, github or remote, branch, purpose and tickets."),(typeof e.name!="string"||!e.name.trim()||e.name.length>60)&&d("Name must be 1\u201360 characters."),e.purpose!==void 0&&(typeof e.purpose!="string"||e.purpose.length>240)&&d("Purpose must be at most 240 characters.");const t=e.name.trim().replace(/\s+/g," "),s=I(t);E.test(s)||d("Name must contain a letter or digit for its route.");const{github:r,remote:l}=q(e),m=P(e.branch);let a=e.tickets===void 0?{source:"internal"}:e.tickets;l&&a?.source==="github"&&d("GitHub issues need a repo on GitHub; pick Internal or Linear.");let n;if(a?.source==="linear"&&typeof a=="object"&&Object.hasOwn(a,"new_project")){const{new_project:i,...g}=a;n=typeof i=="string"?i.trim():"",(!n||n.length>80)&&d("Name the new Linear project."),a={...g,project:n}}return{route:s,title:t,github:r,remote:l,branch:m,tickets:A(a),newProject:n,purpose:(e.purpose||"").trim().replace(/\s+/g," ")}}o(T,"details");function q(e){return e.github===void 0==(e.remote===void 0)&&d("Send the repo as github or as remote."),e.github!==void 0?{github:L(e.github)}:{remote:_(e.remote).remote}}o(q,"repoInput");const S=o((e,t)=>Object.hasOwn(e.sites||{},t)||Object.hasOwn(e.starting||{},t)||Object.hasOwn(e.closed||{},t),"taken"),N="That board route already exists. Reopen it from Closed boards if it is closed.",D=o(e=>`'${e.replaceAll("'","'\\''")}'`,"quote"),H=o(({purpose:e})=>`--template board --items data/wave.json${e?` --description ${D(e)}`:""}`,"joinFlags");function $(e){const t=String(e).split(`
2
- `).map(s=>s.trim()).find(Boolean)||"claude did not start";return t.length>200?`${t.slice(0,199)}\u2026`:t}o($,"errorLine");async function R(e,t){await b(async()=>{const s=h(),r=u(s.starting,e);!r||u(s.sites,e)||(s.starting[e]=t.phase?{...r,phase:t.phase}:t.error?{...r,error:t.error}:{...r,session:t.session,label:t.name,since:new Date().toISOString(),repo:t.repo,...t.model?{model:t.model}:{}},await w(s))})}o(R,"recordStart");const v=o(({title:e,purpose:t,repo:s,github:r,remote:l,branch:m,tickets:a},n)=>({title:e,purpose:t,repo:s,...r?{github:r}:{remote:l},branch:m,tickets:a,phase:n.cloned(r||l)?"starting":"cloning",asked_at:new Date().toISOString()}),"startingRecord");async function x(e,{purpose:t,repo:s,github:r,remote:l,branch:m},{run:a,live:n,repos:i,changed:g}){let f;try{await i.ensure(r||l,m),await R(e,{phase:"starting"}),g();const c=await F(e,{repo:s,prompt:H({purpose:t}),run:a,live:n,clones:i.base});f={session:c.session,name:c.name,repo:c.repo,model:c.model}}catch(c){f={error:$(c.message)}}try{await R(e,f)}catch(c){return{error:$(`could not record the start: ${c.message}`)}}return f.error?{error:f.error}:{session:f.session}}o(x,"start");async function X(e,{run:t,live:s,repos:r=O(),changed:l=o(()=>{},"changed"),linear:m}={}){const a=T(e),{route:n,github:i,remote:g,newProject:f}=a;let{tickets:c}=a;const j=r.path(i||g),k=f===void 0?null:m||M();if(k){S(h(),n)&&d(N);try{c={...c,project:(await k.createProject(c.team,f)).id}}catch(p){d(p.message,p.status||502)}}return await b(async()=>{const p=h();S(p,n)&&d(N),p.starting={...p.starting,[n]:v({...a,repo:j,tickets:c},r)},await w(p)}),{route:n,started:x(n,{...a,repo:j},{run:t,live:s,repos:r,changed:l})}}o(X,"requestBoard");function y(e,t,s){const r=u(e.starting,t);return(!r||u(e.sites,t)||!C(r))&&d(`Only a board that didn\u2019t start can be ${s}.`),r}o(y,"failedStart");async function Y(e,{run:t,live:s,stop:r,repos:l=O(),changed:m=o(()=>{},"changed")}={}){const a=y(h(),e,"retried");a.branch||d("Only a board asked for with New board can be retried; make it again with New board.");try{await G(a,{live:s,stop:r})}catch(i){d(`Couldn\u2019t stop its earlier session: ${i.message}`,502)}const n=await b(async()=>{const i=h(),g=y(i,e,"retried");return i.starting[e]=v({...g,purpose:g.purpose||""},l),await w(i),i.starting[e]});return{route:e,started:x(e,n,{run:t,live:s,repos:l,changed:m})}}o(Y,"retryBoard");async function Z(e){return b(async()=>{const t=h();return y(t,e,"dismissed"),delete t.starting[e],Object.keys(t.starting).length||delete t.starting,await w(t),{route:e}})}o(Z,"dismissBoard");export{Z as dismissBoard,q as repoInput,X as requestBoard,Y as retryBoard};
1
+ var P=Object.defineProperty;var i=(e,t)=>P(e,"name",{value:t,configurable:!0});import{ticketSource as $}from"../board/tickets.mjs";import{isListening as v,listBoards as B,readManifest as h,withManifestLock as u,writeManifest as w}from"./boards.mjs";import{ROUTE_RE as D,routeFromName as L,routeRecord as j}from"./routes.mjs";import{repoService as k,validateGithub as M,validateBranch as A,validateRemote as G}from"./repos.mjs";import{startFailed as C,startSession as F,stopSession as T}from"./sessions.mjs";import{LINEAR_NEEDS_KEY as q,linearWorks as H,savedClient as K}from"./linear.mjs";const d=i((e,t=400)=>{throw Object.assign(new Error(e),{status:t})},"refuse");function Y(e){(!e||typeof e!="object"||Array.isArray(e)||Object.keys(e).some(c=>!["name","github","remote","branch","purpose","tickets"].includes(c)))&&d("Send only name, github or remote, branch, purpose and tickets."),(typeof e.name!="string"||!e.name.trim()||e.name.length>60)&&d("Name must be 1\u201360 characters."),e.purpose!==void 0&&(typeof e.purpose!="string"||e.purpose.length>240)&&d("Purpose must be at most 240 characters.");const t=e.name.trim().replace(/\s+/g," "),n=L(t);D.test(n)||d("Name must contain a letter or digit for its route.");const{github:r,remote:s}=U(e),f=A(e.branch);let a=e.tickets===void 0?{source:"internal"}:e.tickets;s&&a?.source==="github"&&d("GitHub issues need a repo on GitHub; pick Internal or Linear.");let o;if(a?.source==="linear"&&typeof a=="object"&&Object.hasOwn(a,"new_project")){const{new_project:c,...m}=a;o=typeof c=="string"?c.trim():"",(!o||o.length>80)&&d("Name the new Linear project."),a={...m,project:o}}return{route:n,title:t,github:r,remote:s,branch:f,tickets:$(a),newProject:o,purpose:(e.purpose||"").trim().replace(/\s+/g," ")}}i(Y,"details");function U(e){return e.github===void 0==(e.remote===void 0)&&d("Send the repo as github or as remote."),e.github!==void 0?{github:M(e.github)}:{remote:G(e.remote).remote}}i(U,"repoInput");const _=i((e,t)=>Object.hasOwn(e.sites||{},t)||Object.hasOwn(e.starting||{},t)||Object.hasOwn(e.closed||{},t),"taken"),E="That board route already exists. Reopen it from Closed boards if it is closed.",V=i(e=>`'${e.replaceAll("'","'\\''")}'`,"quote"),W=i(({purpose:e})=>`--template board --items data/wave.json${e?` --description ${V(e)}`:""}`,"joinFlags");function R(e){const t=String(e).split(`
2
+ `).map(n=>n.trim()).find(Boolean)||"claude did not start";return t.length>200?`${t.slice(0,199)}\u2026`:t}i(R,"errorLine");async function N(e,t){await u(async()=>{const n=h(),r=j(n.starting,e);!r||j(n.sites,e)||t.phase&&t.phase===r.phase||(n.starting[e]=t.phase?{...r,phase:t.phase,phase_at:new Date().toISOString()}:t.error?{...r,error:t.error}:{...r,session:t.session,label:t.name,since:new Date().toISOString(),repo:t.repo,...t.model?{model:t.model}:{}},await w(n))})}i(N,"recordStart");const I=i(({title:e,purpose:t,repo:n,github:r,remote:s,branch:f,tickets:a},o)=>({title:e,purpose:t,repo:n,...r?{github:r}:{remote:s},branch:f,tickets:a,phase:o.cloned(r||s)?"starting":"cloning",asked_at:new Date().toISOString()}),"startingRecord"),O=new Map,ne=i(e=>O.get(e),"clonePercent"),z=1e3;async function x(e,{purpose:t,repo:n,github:r,remote:s,branch:f},{run:a,live:o,repos:c,changed:m}){let g,p=-1/0;const y=i(l=>{O.set(e,l),!(Date.now()-p<z)&&(p=Date.now(),m())},"progress");try{try{await c.ensure(r||s,f,{progress:y})}finally{O.delete(e)}await N(e,{phase:"starting"}),m();const l=await F(e,{repo:n,prompt:W({purpose:t}),run:a,live:o,clones:c.base});g={session:l.session,name:l.name,repo:l.repo,model:l.model}}catch(l){g={error:R(l.message)}}try{await N(e,g)}catch(l){return{error:R(`could not record the start: ${l.message}`)}}return g.error?{error:g.error}:{session:g.session}}i(x,"start");async function se(e,{run:t,live:n,repos:r=k(),changed:s=i(()=>{},"changed"),linear:f}={}){const a=Y(e),{route:o,github:c,remote:m,newProject:g}=a;let{tickets:p}=a;p.source==="linear"&&!f&&!H()&&d(q);const y=r.path(c||m),l=g===void 0?null:f||K();if(l){_(h(),o)&&d(E);try{p={...p,project:(await l.createProject(p.team,g)).id}}catch(b){d(b.message,b.status||502)}}return await u(async()=>{const b=h();_(b,o)&&d(E),b.starting={...b.starting,[o]:I({...a,repo:y,tickets:p},r)},await w(b)}),{route:o,started:x(o,{...a,repo:y},{run:t,live:n,repos:r,changed:s})}}i(se,"requestBoard");function S(e,t,n){const r=j(e.starting,t);return(!r||j(e.sites,t)&&!r.joined_at||!C(r))&&d(`Only a board that didn\u2019t start can be ${n}.`),r}i(S,"failedStart");async function ae(e,{run:t,live:n,stop:r,repos:s=k(),changed:f=i(()=>{},"changed")}={}){const a=S(h(),e,"retried");a.joined_at&&d("Its session joined; a board that is not listening yet can only be dismissed."),a.branch||d("Only a board asked for with New board can be retried; make it again with New board.");try{await T(a,{live:n,stop:r})}catch(c){d(`Couldn\u2019t stop its earlier session: ${c.message}`,502)}const o=await u(async()=>{const c=h(),m=S(c,e,"retried");return c.starting[e]=I({...m,repo:s.path(m.github||m.remote),purpose:m.purpose||""},s),await w(c),c.starting[e]});return{route:e,started:x(e,o,{run:t,live:n,repos:s,changed:f})}}i(ae,"retryBoard");async function ie(e){return u(async()=>{const t=h();return S(t,e,"dismissed"),delete t.starting[e],Object.keys(t.starting).length||delete t.starting,await w(t),{route:e}})}i(ie,"dismissBoard");async function oe({listening:e=v}={}){return Object.values(h().starting||{}).some(t=>t.joined_at)?u(async()=>{const t=h(),n=new Map(B(t).map(s=>[s.route,s])),r=Object.entries(t.starting||{}).filter(([s,f])=>f.joined_at&&(!n.has(s)||e(n.get(s)))).map(([s])=>s);if(!r.length)return[];for(const s of r)delete t.starting[s];return Object.keys(t.starting).length||delete t.starting,await w(t),r}):[]}i(oe,"settleStarts");export{ne as clonePercent,ie as dismissBoard,U as repoInput,se as requestBoard,ae as retryBoard,oe as settleStarts};
@@ -0,0 +1 @@
1
+ var g=Object.defineProperty;var w=(r,s)=>g(r,"name",{value:s,configurable:!0});import{WAVES as C}from"./boards.mjs";import{parseCheck as l,runCheck as y}from"../shared/owner-check.mjs";import{passOwnerCheck as O,breakOwnerCheck as b}from"../board/store.mjs";const v=3;async function A(r,s,u){const m=s.ownerChecks||{},i={},d=new Set;for(const e of r)for(const{slug:f,wave:p}of e.waves)for(const n of p.items){const c=n.owner_check;if(!c||c.broken||!n.owner_step||!["done","merged"].includes(n.status))continue;const o=JSON.stringify([e.kind==="waves"?C:e.dir,f,n.id,c.check]);if(d.has(o))continue;d.add(o);const t=`${f}/${n.id}`;let a;try{a=await y(l(c.check),{since:c.since})}catch(k){const h=(m[o]||0)+1;if(h<v){i[o]=h;continue}try{await b(e.dir,t,c.check,k.message)}catch($){i[o]=h,u.push(`${e.route}: could not mark ${t}'s check broken (${$.message})`)}continue}if(a.passed)try{await O(e.dir,t,c.check,a.found)}catch(k){u.push(`${e.route}: could not close ${t} on its passing check (${k.message})`)}}s.ownerChecks=i}w(A,"runOwnerChecks");export{v as BROKEN_AFTER,A as runOwnerChecks};
@@ -0,0 +1,2 @@
1
+ #!/usr/bin/env node
2
+ const o=`http://127.0.0.1:${Number(process.env.KICKFLIP_HUB_PORT||4500)}`;let t="";for await(const e of process.stdin)t+=e;await fetch(`${o}/_hub/permission-denied`,{method:"POST",headers:{"content-type":"application/json"},body:t,signal:AbortSignal.timeout(5e3)}).catch(()=>{});
package/hub/pr-report.mjs CHANGED
@@ -1 +1,3 @@
1
- var d=Object.defineProperty;var i=(t,n)=>d(t,"name",{value:n,configurable:!0});import{execFile as f}from"node:child_process";const b="state,isDraft,reviewDecision,mergeable,mergeStateStatus,headRefOid,statusCheckRollup",w="/opt/homebrew/bin:/usr/local/bin:/usr/bin:/bin:/usr/sbin:/sbin",h=i(t=>String(t).trim().split(/\r?\n/)[0].slice(0,200),"firstLine");function p(t,n=process.env){return new Promise((o,s)=>{f("gh",t,{env:{...n,PATH:[n.PATH,w].filter(Boolean).join(":")},timeout:9e4,maxBuffer:16*1024*1024},(l,a,u)=>{if(l)return s(new Error(h(u||l.message)));try{o(JSON.parse(a))}catch(r){s(new Error(h(`unreadable output (${r.message})`)))}})})}i(p,"gh");async function E({repo:t,pr:n,since:o}){let s;try{s=await p(["pr","view",String(n),"--repo",t,"--json",b])}catch(m){throw new Error(`${t}#${n}: ${m.message}`)}const l=i(m=>p(["api","--paginate","--slurp",`repos/${t}/${m}`]).then(c=>c.flat()).catch(()=>[]),"list"),[a,u,r]=await Promise.all([l(`issues/${n}/comments?since=${encodeURIComponent(o)}`),l(`pulls/${n}/comments?since=${encodeURIComponent(o)}`),l(`pulls/${n}/reviews`)]);return{view:s,issueComments:a,reviewComments:u,reviews:r}}i(E,"fetchPR");function R({view:t,issueComments:n,reviewComments:o,reviews:s},{pr:l,since:a,me:u}){const r=i((e,g)=>({kind:g,id:e.id??null,login:e.user?.login??null,at:e.created_at??null,body:e.body??null}),"comment"),m=[...n.map(e=>({...r(e,"issue_comment"),url:e.html_url??null})),...o.map(e=>({...r(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})),...s.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>a).map(e=>({...e,who:e.login===u?/<!-- claude-bridge -->/.test(e.body??"")?"me":"user":/codex/.test(e.login)?"codex":/\[bot\]$|^linear/.test(e.login)?"bot":"reviewer"})).filter(e=>e.who!=="me"&&e.who!=="bot").sort((e,g)=>e.at<g.at?-1:e.at>g.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:m}}i(R,"reportOf");async function $({env:t}={}){const n=await p(["api","user"],t);if(typeof n?.login!="string"||!n.login)throw new Error("gh api user: missing login");return n.login}i($,"githubLogin");export{b as PR_VIEW_FIELDS,E as fetchPR,$ as githubLogin,R as reportOf};
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}
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};
package/hub/presence.mjs CHANGED
@@ -1 +1 @@
1
- var l=Object.defineProperty;var s=(r,e)=>l(r,"name",{value:e,configurable:!0});import{execFile as d}from"node:child_process";const m=3*6e4,i=s(r=>new Promise((e,o)=>{d("ioreg",r,{timeout:5e3,maxBuffer:32*1024*1024},(t,n)=>t?o(t):e(n))}),"ioreg");async function u(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(c=>Number(c[1])/1e6);if(!n.length)return{atMac:!1,error:"ioreg reported no HIDIdleTime"};const a=Math.round(Math.min(...n));return{atMac:!t&&a<m,locked:t,idleMs:a}}catch(e){return{atMac:!1,error:String(e.message||e)}}}s(u,"readPresence");function I({run:r=i,ttl:e=5e3,clock:o=Date.now}={}){let t=null;return()=>((!t||o()-t.at>=e)&&(t={at:o(),reading:u(r)}),t.reading)}s(I,"presenceReader");const p=I();export{m as IDLE_LIMIT_MS,p as presence,I as presenceReader,u as readPresence};
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};
package/hub/prompts.mjs CHANGED
@@ -1,2 +1,2 @@
1
- var j=Object.defineProperty;var s=(e,t)=>j(e,"name",{value:t,configurable:!0});import S from"node:fs";import y from"node:os";import _ from"node:path";import E from"node:crypto";import{withFileLock as D}from"./locks.mjs";import{routeRecord as l}from"./routes.mjs";const f=_.join(y.homedir(),".claude","kickflip","prompts.json"),v=10*6e4,z=25e3,O=6e4,V="No answer from the owner; ask this as an open question on your board's card instead.",X="The hub can't be reached, so the owner can't answer this prompt; ask this as an open question on your board's card instead.",Y="The hub dropped this prompt; ask this as an open question on your board's card instead.",k="The owner denied this from the hub.",F="The owner skipped this question; ask it as an open question on your board's card instead.",m="AskUserQuestion",d={Edit:"edit",MultiEdit:"edit",Write:"write",Read:"read",NotebookEdit:"edit"},h=s((e,t)=>{const r=String(e??"").replace(/\s+/g," ").trim();return r.length>t?`${r.slice(0,t-1)}\u2026`:r},"cut"),w=s(e=>e?.file_path||e?.notebook_path||"","fileOf"),N=s(e=>{try{return new URL(e).host}catch{return String(e||"")}},"hostOf");function W(e,t={}){if(e==="Bash")return`run ${h(t.command,60)}`;if(l(d,e))return`${l(d,e)} ${_.basename(w(t))||"a file"}`;if(e==="WebFetch")return`fetch ${N(t.url)}`;if(e==="WebSearch")return`search the web for "${h(t.query,50)}"`;const r=/^mcp__(.+?)__(.+)$/.exec(e||"");return r?`use ${r[1]} ${r[2]}`:`use ${e}`}s(W,"actionOf");function T(e,t={}){if(e==="Bash")return String(t.command??"");if(l(d,e))return w(t);if(e==="WebFetch")return String(t.url??"");if(e==="WebSearch")return String(t.query??"");const r=JSON.stringify(t);return r==="{}"?"":h(r,600)}s(T,"detailOf");function P(e){const t=(Array.isArray(e?.permission_suggestions)?e.permission_suggestions:[]).filter(n=>n?.type==="addRules"&&n.behavior==="allow"&&Array.isArray(n.rules)&&n.rules.length).map(n=>({type:"addRules",rules:n.rules,behavior:"allow",destination:"session"}));if(t.length)return t;const r=e?.tool_name,o=e?.tool_input||{},i=r==="Bash"&&o.command?o.command:l(d,r)&&w(o).startsWith("/")?`/${w(o)}`:r==="WebFetch"&&o.url?`domain:${N(o.url)}`:null;return[{type:"addRules",rules:[i?{toolName:r,ruleContent:i}:{toolName:r}],behavior:"allow",destination:"session"}]}s(P,"alwaysRules");const R=s(e=>e.flatMap(t=>t.rules).map(t=>t.ruleContent?`${t.toolName}(${t.ruleContent})`:t.toolName).join(", "),"ruleText"),b=s(e=>Number.isInteger(e?.pid)&&e.pid>0&&typeof e.procStart=="string"&&Number.isFinite(Date.parse(`${e.procStart.trim().replace(/\s+/g," ")} UTC`))?{pid:e.pid,procStart:e.procStart}:null,"runtimeOf"),$=s((e,t)=>!!(b(e)&&b(t)&&e.pid===t.pid&&e.procStart===t.procStart),"sameRuntime"),p=s(e=>typeof e?.owner?.since=="string"&&e.owner.since?e.owner.since:null,"ownerSince"),x=s((e,t,r)=>!!(t?.owner?.session===r&&e.ownerSince&&e.ownerSince===p(t)),"sameOwner");function B(e,t){const r=t?.tool_name;if(r===m||!e||e.toolName!==r||typeof r!="string")return!1;const o=e.ruleContent;if(o===void 0)return!0;if(typeof o!="string"||!o)return!1;const i=t.tool_input||{};if(r==="Bash")return typeof i.command=="string"&&o===i.command;if(l(d,r))return o.startsWith("//")&&!/[\*?\[\]{}]/.test(o)&&o.slice(1)===w(i);if(r==="WebFetch")try{const n=new URL(i.url);return["http:","https:"].includes(n.protocol)&&o===`domain:${n.host}`}catch{return!1}return!1}s(B,"matchesRule");function Z(e,{running:t,boards:r}){let o=!1;for(const[i,n]of Object.entries(e.remembered||{})){const a=r.find(u=>n.route?u.route===n.route:u.owner?.session===i);!$(n.runtime,b(t.get(i)))||n.route&&!x(n,a,i)||a&&!p(a)?(delete e.remembered[i],o=!0):n.route===null&&a&&(n.route=a.route,n.ownerSince=p(a),o=!0)}return o}s(Z,"pruneRemembered");function ee(e,t,r){if(t.answer?.choice!=="always"||t.tool===m||!t.session||!t.permissions||!$(t.runtime,b(r.running.get(t.session))))return!1;const o=l(e.remembered,t.session),i=t.route||o?.route,n=i&&r.boards.find(c=>c.route===i)||!t.route&&r.boards.find(c=>c.owner?.session===t.session);if(t.route&&!x(t,n,t.session)||n&&(n.owner?.session!==t.session||!p(n)))return!1;e.remembered||={};const u=o&&$(o.runtime,t.runtime)&&(o.route?o.route===n?.route&&x(o,n,t.session):!0)?[...o.rules]:[];for(const c of t.permissions.flatMap(g=>g.rules))u.some(g=>g.toolName===c.toolName&&g.ruleContent===c.ruleContent)||u.push(c);return Object.defineProperty(e.remembered,t.session,{value:{route:n?.route||null,ownerSince:p(n),runtime:t.runtime,rules:u},enumerable:!0,writable:!0,configurable:!0}),!0}s(ee,"rememberRules");function te(e,t){return!t.session_id||t.tool_name===m||!l(e.remembered,t.session_id)?null:l(e.remembered,t.session_id).rules.some(r=>B(r,t))?{choice:"allow"}:null}s(te,"rememberedAnswer");function re(e,t=q()){return Object.entries(t.remembered||{}).filter(([,r])=>r.route===e).map(([r,o])=>({session:r,rules:o.rules.map(i=>R([{rules:[i]}]))}))}s(re,"rememberedFor");function oe(e,t){return{hookSpecificOutput:{hookEventName:"PermissionRequest",decision:t?.choice==="deny"?{behavior:"deny",message:t.message||k}:t?.choice==="always"&&e?.tool_name!==m?{behavior:"allow",updatedPermissions:P(e)}:e?.tool_name===m?{behavior:"allow",updatedInput:{...e.tool_input,answers:t?.answers||{}}}:{behavior:"allow"}}}}s(oe,"hookOutput");const A=s(e=>e&&e.startsWith(y.homedir()+"/")?`~${e.slice(y.homedir().length)}`:e||"","tilde");function C(e){return(Array.isArray(e?.questions)?e.questions:[]).filter(r=>typeof r?.question=="string").map(r=>({question:r.question,header:typeof r.header=="string"?r.header:"",multiSelect:r.multiSelect===!0,options:(Array.isArray(r.options)?r.options:[]).filter(o=>typeof o?.label=="string").map(o=>({label:o.label,description:typeof o.description=="string"?o.description:""}))}))}s(C,"questionsOf");function ne(e,{board:t,live:r,now:o=new Date}){const i=String(e.tool_name),n=i===m,a=n?null:P(e),u=o.toISOString();return{id:"p"+E.randomBytes(6).toString("hex"),session:String(e.session_id||""),route:t?.route||null,title:t?.title||A(e.cwd)||"A background session",cwd:String(e.cwd||""),tool:i,action:n?null:W(i,e.tool_input),detail:n?"":T(i,e.tool_input),rule:n?null:R(a),questions:n?C(e.tool_input):null,permissions:a,runtime:b(r),ownerSince:p(t),at:u,deadline:new Date(o.getTime()+v).toISOString(),seen:u,answer:null}}s(ne,"promptRecord");function se(e,t,r=new Date){const o=r.toISOString();if(e.questions){if(t?.choice==="deny")return{answer:{choice:"deny",message:F,at:o}};const i=t?.answers&&typeof t.answers=="object"?t.answers:{};for(const n of e.questions){const a=String(l(i,n.question)??"").split(n.multiSelect?", ":"\0").filter(Boolean);if(!a.length)return{error:`Pick an answer to "${n.question}".`};if(!a.every(u=>n.options.some(c=>c.label===u)))return{error:`"${l(i,n.question)}" isn't an option of "${n.question}".`}}return{answer:{choice:"allow",answers:Object.fromEntries(e.questions.map(n=>[n.question,String(l(i,n.question))])),at:o}}}return["allow","always","deny"].includes(t?.choice)?{answer:{choice:t.choice,...t.choice==="deny"?{message:k}:{},at:o}}:{error:"Pass choice allow, always or deny."}}s(se,"answerOf");function ie(e,{now:t=Date.now(),aliveSince:r=0}={}){return Object.values(e.prompts||{}).filter(o=>t-Date.parse(o.deadline)>O?!0:o.answer?t-Date.parse(o.answer.at)>O:t-Math.max(Date.parse(o.seen),r)>O)}s(ie,"expired");function q(){try{const e=JSON.parse(S.readFileSync(f,"utf8"));return{prompts:e?.prompts&&typeof e.prompts=="object"?e.prompts:{},remembered:e?.remembered&&typeof e.remembered=="object"?e.remembered:{}}}catch{return{prompts:{},remembered:{}}}}s(q,"readPrompts");function I(e){const t=`${f}.${process.pid}.tmp`;S.mkdirSync(_.dirname(f),{recursive:!0}),S.writeFileSync(t,JSON.stringify(e,null,2)+`
1
+ var j=Object.defineProperty;var s=(e,t)=>j(e,"name",{value:t,configurable:!0});import S from"node:fs";import y from"node:os";import _ from"node:path";import E from"node:crypto";import{withFileLock as D}from"./locks.mjs";import{routeRecord as l}from"./routes.mjs";const f=_.join(y.homedir(),".claude","kickflip","prompts.json"),v=10*6e4,z=25e3,O=6e4,V="No answer from the owner; ask this as an open question on your board's card instead.",X="The hub can't be reached, so the owner can't answer this prompt; ask this as an open question on your board's card instead.",Y="The hub dropped this prompt; ask this as an open question on your board's card instead.",k="The owner denied this from the hub.",F="The owner skipped this question; ask it as an open question on your board's card instead.",m="AskUserQuestion",d={Edit:"edit",MultiEdit:"edit",Write:"write",Read:"read",NotebookEdit:"edit"},h=s((e,t)=>{const r=String(e??"").replace(/\s+/g," ").trim();return r.length>t?`${r.slice(0,t-1)}\u2026`:r},"cut"),w=s(e=>e?.file_path||e?.notebook_path||"","fileOf"),N=s(e=>{try{return new URL(e).host}catch{return String(e||"")}},"hostOf");function W(e,t={}){if(e==="Bash")return`run ${h(t.command,60)}`;if(l(d,e))return`${l(d,e)} ${_.basename(w(t))||"a file"}`;if(e==="WebFetch")return`fetch ${N(t.url)}`;if(e==="WebSearch")return`search the web for "${h(t.query,50)}"`;const r=/^mcp__(.+?)__(.+)$/.exec(e||"");return r?`use ${r[1]} ${r[2]}`:`use ${e}`}s(W,"actionOf");function T(e,t={}){if(e==="Bash")return String(t.command??"");if(l(d,e))return w(t);if(e==="WebFetch")return String(t.url??"");if(e==="WebSearch")return String(t.query??"");const r=JSON.stringify(t);return r==="{}"?"":h(r,600)}s(T,"detailOf");function P(e){const t=(Array.isArray(e?.permission_suggestions)?e.permission_suggestions:[]).filter(n=>n?.type==="addRules"&&n.behavior==="allow"&&Array.isArray(n.rules)&&n.rules.length).map(n=>({type:"addRules",rules:n.rules,behavior:"allow",destination:"session"}));if(t.length)return t;const r=e?.tool_name,o=e?.tool_input||{},i=r==="Bash"&&o.command?o.command:l(d,r)&&w(o).startsWith("/")?`/${w(o)}`:r==="WebFetch"&&o.url?`domain:${N(o.url)}`:null;return[{type:"addRules",rules:[i?{toolName:r,ruleContent:i}:{toolName:r}],behavior:"allow",destination:"session"}]}s(P,"alwaysRules");const R=s(e=>e.flatMap(t=>t.rules).map(t=>t.ruleContent?`${t.toolName}(${t.ruleContent})`:t.toolName).join(", "),"ruleText"),b=s(e=>Number.isInteger(e?.pid)&&e.pid>0&&typeof e.procStart=="string"&&Number.isFinite(Date.parse(`${e.procStart.trim().replace(/\s+/g," ")} UTC`))?{pid:e.pid,procStart:e.procStart}:null,"runtimeOf"),$=s((e,t)=>!!(b(e)&&b(t)&&e.pid===t.pid&&e.procStart===t.procStart),"sameRuntime"),p=s(e=>typeof e?.owner?.since=="string"&&e.owner.since?e.owner.since:null,"ownerSince"),x=s((e,t,r)=>!!(t?.owner?.session===r&&e.ownerSince&&e.ownerSince===p(t)),"sameOwner");function B(e,t){const r=t?.tool_name;if(r===m||!e||e.toolName!==r||typeof r!="string")return!1;const o=e.ruleContent;if(o===void 0)return!0;if(typeof o!="string"||!o)return!1;const i=t.tool_input||{};if(r==="Bash")return typeof i.command=="string"&&o===i.command;if(l(d,r))return o.startsWith("//")&&!/[\*?\[\]{}]/.test(o)&&o.slice(1)===w(i);if(r==="WebFetch")try{const n=new URL(i.url);return["http:","https:"].includes(n.protocol)&&o===`domain:${n.host}`}catch{return!1}return!1}s(B,"matchesRule");function Z(e,{running:t,boards:r}){if(!t)return!1;let o=!1;for(const[i,n]of Object.entries(e.remembered||{})){const a=r.find(u=>n.route?u.route===n.route:u.owner?.session===i);!$(n.runtime,b(t.get(i)))||n.route&&!x(n,a,i)||a&&!p(a)?(delete e.remembered[i],o=!0):n.route===null&&a&&(n.route=a.route,n.ownerSince=p(a),o=!0)}return o}s(Z,"pruneRemembered");function ee(e,t,r){if(t.answer?.choice!=="always"||t.tool===m||!t.session||!t.permissions||!$(t.runtime,b(r.running?.get(t.session))))return!1;const o=l(e.remembered,t.session),i=t.route||o?.route,n=i&&r.boards.find(c=>c.route===i)||!t.route&&r.boards.find(c=>c.owner?.session===t.session);if(t.route&&!x(t,n,t.session)||n&&(n.owner?.session!==t.session||!p(n)))return!1;e.remembered||={};const u=o&&$(o.runtime,t.runtime)&&(o.route?o.route===n?.route&&x(o,n,t.session):!0)?[...o.rules]:[];for(const c of t.permissions.flatMap(g=>g.rules))u.some(g=>g.toolName===c.toolName&&g.ruleContent===c.ruleContent)||u.push(c);return Object.defineProperty(e.remembered,t.session,{value:{route:n?.route||null,ownerSince:p(n),runtime:t.runtime,rules:u},enumerable:!0,writable:!0,configurable:!0}),!0}s(ee,"rememberRules");function te(e,t){return!t.session_id||t.tool_name===m||!l(e.remembered,t.session_id)?null:l(e.remembered,t.session_id).rules.some(r=>B(r,t))?{choice:"allow"}:null}s(te,"rememberedAnswer");function re(e,t=q()){return Object.entries(t.remembered||{}).filter(([,r])=>r.route===e).map(([r,o])=>({session:r,rules:o.rules.map(i=>R([{rules:[i]}]))}))}s(re,"rememberedFor");function oe(e,t){return{hookSpecificOutput:{hookEventName:"PermissionRequest",decision:t?.choice==="deny"?{behavior:"deny",message:t.message||k}:t?.choice==="always"&&e?.tool_name!==m?{behavior:"allow",updatedPermissions:P(e)}:e?.tool_name===m?{behavior:"allow",updatedInput:{...e.tool_input,answers:t?.answers||{}}}:{behavior:"allow"}}}}s(oe,"hookOutput");const A=s(e=>e&&e.startsWith(y.homedir()+"/")?`~${e.slice(y.homedir().length)}`:e||"","tilde");function C(e){return(Array.isArray(e?.questions)?e.questions:[]).filter(r=>typeof r?.question=="string").map(r=>({question:r.question,header:typeof r.header=="string"?r.header:"",multiSelect:r.multiSelect===!0,options:(Array.isArray(r.options)?r.options:[]).filter(o=>typeof o?.label=="string").map(o=>({label:o.label,description:typeof o.description=="string"?o.description:""}))}))}s(C,"questionsOf");function ne(e,{board:t,live:r,now:o=new Date}){const i=String(e.tool_name),n=i===m,a=n?null:P(e),u=o.toISOString();return{id:"p"+E.randomBytes(6).toString("hex"),session:String(e.session_id||""),route:t?.route||null,title:t?.title||A(e.cwd)||"A background session",cwd:String(e.cwd||""),tool:i,action:n?null:W(i,e.tool_input),detail:n?"":T(i,e.tool_input),rule:n?null:R(a),questions:n?C(e.tool_input):null,permissions:a,runtime:b(r),ownerSince:p(t),at:u,deadline:new Date(o.getTime()+v).toISOString(),seen:u,answer:null}}s(ne,"promptRecord");function se(e,t,r=new Date){const o=r.toISOString();if(e.questions){if(t?.choice==="deny")return{answer:{choice:"deny",message:F,at:o}};const i=t?.answers&&typeof t.answers=="object"?t.answers:{};for(const n of e.questions){const a=String(l(i,n.question)??"").split(n.multiSelect?", ":"\0").filter(Boolean);if(!a.length)return{error:`Pick an answer to "${n.question}".`};if(!a.every(u=>n.options.some(c=>c.label===u)))return{error:`"${l(i,n.question)}" isn't an option of "${n.question}".`}}return{answer:{choice:"allow",answers:Object.fromEntries(e.questions.map(n=>[n.question,String(l(i,n.question))])),at:o}}}return["allow","always","deny"].includes(t?.choice)?{answer:{choice:t.choice,...t.choice==="deny"?{message:k}:{},at:o}}:{error:"Pass choice allow, always or deny."}}s(se,"answerOf");function ie(e,{now:t=Date.now(),aliveSince:r=0}={}){return Object.values(e.prompts||{}).filter(o=>t-Date.parse(o.deadline)>O?!0:o.answer?t-Date.parse(o.answer.at)>O:t-Math.max(Date.parse(o.seen),r)>O)}s(ie,"expired");function q(){try{const e=JSON.parse(S.readFileSync(f,"utf8"));return{prompts:e?.prompts&&typeof e.prompts=="object"?e.prompts:{},remembered:e?.remembered&&typeof e.remembered=="object"?e.remembered:{}}}catch{return{prompts:{},remembered:{}}}}s(q,"readPrompts");function I(e){const t=`${f}.${process.pid}.tmp`;S.mkdirSync(_.dirname(f),{recursive:!0}),S.writeFileSync(t,JSON.stringify(e,null,2)+`
2
2
  `),S.renameSync(t,f)}s(I,"savePrompts");const L=s(e=>D(f,e),"withPromptsLock");function ae(e){return L(()=>{const t=q(),r=e(t);return I(t),r})}s(ae,"updatePrompts");function ue(e,t){const r=new Set(t.filter(Boolean));for(const o of r)delete e.remembered[o];for(const[o,i]of Object.entries(e.prompts))r.has(i.session)&&delete e.prompts[o]}s(ue,"dropSessionPrompts");function ce(e,t=q()){return Object.values(t.prompts).filter(r=>!r.answer&&r.route===e).sort((r,o)=>r.at.localeCompare(o.at))}s(ce,"pendingFor");const le=s(({id:e,title:t,cwd:r,tool:o,action:i,detail:n,rule:a,questions:u,at:c})=>({id:e,title:t,cwd:A(r),tool:o,action:i,detail:n,rule:a,questions:u,at:c}),"shown");function me(e){const t=e.questions?.[0];return{title:t?`${e.title} asks: ${h(t.question,90)}`:`${e.title} wants to ${e.action}`,body:t?t.options.map(r=>r.label).join(" \xB7 ")+(e.questions.length>1?` (+${e.questions.length-1} more)`:""):h(e.detail,160)||`Answer in ${e.route?"Needs you":"the hub page"}.`,url:e.route?`/${e.route}/`:"/_hub/",tag:`prompt-${e.id}`}}s(me,"pushOf");export{k as DENIED,Y as DROPPED,O as GONE_AFTER,V as NO_ANSWER,z as POLL_MS,F as SKIPPED,X as UNREACHABLE,v as WAIT_MS,W as actionOf,P as alwaysRules,se as answerOf,ue as dropSessionPrompts,ie as expired,oe as hookOutput,B as matchesRule,ce as pendingFor,ne as promptRecord,Z as pruneRemembered,me as pushOf,q as readPrompts,ee as rememberRules,te as rememberedAnswer,re as rememberedFor,I as savePrompts,le as shown,ae as updatePrompts,L as withPromptsLock};
package/hub/push.mjs CHANGED
@@ -1,2 +1,2 @@
1
- var G=Object.defineProperty;var s=(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,pollingOf as F,flushAt as Z,hold as K,windowAt as tt,notifications as et,snoozeEnd as nt}from"./schedule.mjs";import{storedEnd as ot}from"./duration.mjs";import{presence as I}from"./presence.mjs";import{CODEX_SHARE_VALUES as rt,codexStatus as q,effectiveCodexShare as st}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=s(t=>y.join(t,".claude","kickflip","settings.json"),"settingsFile"),P=s(t=>Buffer.from(t).toString("base64url"),"b64u"),$=s(t=>Buffer.from(t,"base64url"),"unb64u"),B=s((t,e)=>m.createHmac("sha256",t).update(e).digest(),"hmac"),l=s((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}s(L,"vapidKey");function it(){const t=L();return P(Buffer.concat([Buffer.from([4]),$(t.x),$(t.y)]))}s(it,"publicKey");function g(t=O){try{return JSON.parse(u.readFileSync(t,"utf8"))}catch{return[]}}s(g,"readSubs");function bt(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}s(bt,"addSubscription");function Et(t){const e=g(),n=e.filter(o=>o.endpoint!==t);return n.length!==e.length&&l(O,n),n.length}s(Et,"removeSubscription");function jt(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}))}s(jt,"subscriptions");const M=s(t=>!t.agent||/iPhone|iPad|Android/.test(t.agent),"onPhone");async function T(t,e=!1){const n=g();if(t!=="action"||e||!(await I()).atMac)return n;const o=n.filter(i=>!M(i));return o.length?o:n}s(T,"reach");async function Ot(){return{atMac:(await I()).atMac,desktops:g().filter(t=>!M(t)).length}}s(Ot,"pushRoute");function at(t,e){const n=$(t.keys.p256dh),o=m.createECDH("prime256v1"),i=o.generateKeys(),a=B($(t.keys.auth),o.computeSecret(n)),r=B(a,Buffer.concat([Buffer.from("WebPush: info\0"),n,i,Buffer.from([1])])),c=m.randomBytes(16),d=B(c,r),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])}s(at,"encrypt");function ct(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=${it()}`}s(ct,"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{}}}s(R,"hubNeeds");const Bt=s(t=>_(g(),t),"sendAll");async function _(t,e,n){if(!t.length)return[];const o=JSON.stringify({...e,badge:(n||await R()).total}),i=await Promise.all(t.map(async r=>{try{const c=await fetch(r.endpoint,{method:"POST",headers:{authorization:ct(r),"content-encoding":"aes128gcm","content-type":"application/octet-stream",ttl:"86400",urgency:"high"},body:at(r,o),signal:AbortSignal.timeout(15e3)});return{sub:r,status:c.status,error:c.ok?void 0:(await c.text()).slice(0,300)}}catch(c){return{sub:r,status:0,error:String(c.message||c)}}})),a=new Set(i.filter(r=>r.status===404||r.status===410).map(r=>r.sub.endpoint));return a.size&&l(O,g().filter(r=>!a.has(r.endpoint))),i.map(r=>({host:new URL(r.sub.endpoint).host,agent:r.sub.agent,status:r.status,error:r.error,dropped:a.has(r.sub.endpoint)||void 0}))}s(_,"sendTo");const Q=s(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{}}}s(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)}s(U,"writeSettingsFile");const vt=s((t=q(),e=x.homedir())=>st(w(e).codexShare,t),"readCodexShare");function At(t,e=q()){if(!rt.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}s(At,"setCodexShare");const H=["opus","auto","sonnet"],Dt=s((t=x.homedir())=>{const e=w(t).claudeModel;return H.includes(e)?e:"auto"},"readClaudeModel");function Pt(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}s(Pt,"setClaudeModel");function N(t){return F(w(t))}s(N,"readSettings");function $t(){const t=w();if(!t.push)return null;const{push:e,...n}=t,{notifications:o,...i}=F(t);return U({...n,polling:i}),i}s($t,"migrateSettings");function ut(t){const{settings:e,error:n}=Y({notifications:N().notifications,...t});if(n)throw Object.assign(new Error(n),{status:400});const{push:o,...i}=w(),{notifications:a,...r}=e;return U({...i,polling:r,notifications:a}),e}s(ut,"saveSettings");async function Ut(t,e=new Date){const n=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 r=t.snooze==="off"?null:nt(e,t.snooze,n);if(r===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=r&&ot(r)}const{notifications:i}=ut({...n,notifications:o}),a=i.on?0:await S(()=>{const{items:r}=p();return r.length&&l(h,{items:[]}),r.length});return{notifications:i,dropped:a}}s(Ut,"setNotifications");function p(){try{const t=JSON.parse(u.readFileSync(h,"utf8"));return{items:Array.isArray(t.items)?t.items:[]}}catch{return{items:[]}}}s(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{}}}s(S,"withQueueLock");function ft(t=new Date){const e=N(),{items:n}=p(),o=K(t,e);return{settings:e,window:tt(t,e),held:o!==null,hold:o,count:n.length,nextFlushAt:n.length?Z(t,e):null}}s(ft,"queueStatus");const V=["action","prompt","fault"];class dt extends Error{static{s(this,"PushKindError")}}const lt=s(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 Nt(t,{kind:e,urgent:n=!1,immediate:o=!1,at:i=new Date}={}){const a=lt(e);if(a)throw new dt(a);if(!g().length)return{noDevice:!0};const r={title:t.title,body:t.body||"",url:t.url||"/",tag:t.tag||void 0},c=K(i,N());if(c?.why==="off")return{dropped:!0};const d=c!==null;let k=!1;if(n||o&&!d){const f=await _(await T(e,n),r);if(Q(f))return r.tag&&await S(()=>l(h,{items:p().items.filter(A=>A.tag!==r.tag)})),{sent:f};if(n)return{sent:f};k=!0}const b={id:m.randomUUID(),...r,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=ft(i);return{queued:{...E,count:v.count,nextFlushAt:v.nextFlushAt,held:d,hold:c,sendFailed:k}}}s(Nt,"deliver");async function _t(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})}s(_t,"reviseQueued");async function Ct(t,e){const n=new Set(t),o=s(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)),r=i.length-a.length;return r&&l(h,{items:a}),r}):0}s(Ct,"dropQueued");let C=null;function Ft(){return C||=(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 et(t,i.sites)){const r=await _(await T(a.kind),{title:a.title,body:a.body,url:a.url,tag:a.tag},i);o.push(...r),(Q(r)?e:n).push(a)}return n.length&&await S(()=>{const a=p(),r=new Set(a.items.map(d=>d.tag).filter(Boolean)),c=t.filter(d=>n.some(k=>k.members.includes(d.id))&&!(d.tag&&r.has(d.tag)));l(h,{items:[...c,...a.items]})}),{count:t.length,sent:e.length,ok:!n.length,results:o}})().finally(()=>{C=null}),C}s(Ft,"flushQueue");export{H as CLAUDE_MODEL_VALUES,V as PUSH_KINDS,dt as PushKindError,bt as addSubscription,Nt as deliver,Ct as dropQueued,Ft as flushQueue,$t as migrateSettings,it as publicKey,lt as pushKindError,Ot as pushRoute,ft as queueStatus,Dt as readClaudeModel,vt as readCodexShare,N as readSettings,w as readSettingsFile,Et as removeSubscription,_t as reviseQueued,ut as saveSettings,Bt as sendAll,Pt as setClaudeModel,At as setCodexShare,Ut as setNotifications,jt as subscriptions,U as writeSettingsFile};
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};