@lovek4d/kickflip 0.1.0 → 0.4.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 (120) hide show
  1. package/CONTEXT.md +239 -104
  2. package/README.md +54 -4
  3. package/SKILL.md +63 -42
  4. package/bin/doctor.mjs +4 -3
  5. package/bin/install.mjs +3 -3
  6. package/bin/kickflip.mjs +103 -40
  7. package/bin/statusline.mjs +4 -3
  8. package/bin/wait-events.mjs +2 -2
  9. package/board/arch-review.mjs +5 -0
  10. package/board/auto-review.mjs +2 -0
  11. package/board/bin/agent.mjs +1 -1
  12. package/board/bin/board.mjs +371 -82
  13. package/board/bin/builder-stop-hook.mjs +4 -3
  14. package/board/domain.mjs +8 -3
  15. package/board/finish.mjs +3 -0
  16. package/board/headless.mjs +3 -0
  17. package/board/on-host.mjs +1 -0
  18. package/board/permissions.mjs +2 -0
  19. package/board/public/app-arch.js +75 -0
  20. package/board/public/app-asks.js +66 -28
  21. package/board/public/app-banner.js +2 -2
  22. package/board/public/app-cards.js +354 -77
  23. package/board/public/app-core.js +40 -8
  24. package/board/public/app-header.js +33 -8
  25. package/board/public/app-lanes.js +238 -34
  26. package/board/public/app-needs.js +2 -0
  27. package/board/public/app-snapshot.js +45 -11
  28. package/board/public/app-threads.js +20 -21
  29. package/board/public/app.html +6 -2
  30. package/board/public/app.js +114 -33
  31. package/board/public/attach.js +39 -3
  32. package/board/public/dialogs.js +161 -118
  33. package/board/public/hub-kit.js +27 -7
  34. package/board/public/hub.css +26 -33
  35. package/board/public/linear-key.js +143 -0
  36. package/board/public/log.html +48 -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 +14 -10
  42. package/board/public/rules-editor.js +4 -5
  43. package/board/public/shell.js +21 -7
  44. package/board/public/sidebar.css +51 -11
  45. package/board/public/sidebar.js +124 -19
  46. package/board/public/style.css +244 -56
  47. package/board/public/toasts.js +5 -3
  48. package/board/public/ui-cards.js +236 -66
  49. package/board/public/ui-enums.js +5 -4
  50. package/board/public/ui-header.js +23 -14
  51. package/board/public/ui-hub.js +34 -16
  52. package/board/public/ui-links.js +18 -10
  53. package/board/public/ui-markdown.js +1 -1
  54. package/board/public/ui-order.js +21 -0
  55. package/board/public/ui-page-header.js +13 -8
  56. package/board/public/ui-patch.js +53 -0
  57. package/board/public/ui-phase-bars.js +17 -0
  58. package/board/public/ui-prompts.js +8 -8
  59. package/board/public/ui-questions.js +35 -11
  60. package/board/public/ui-sidebar.js +83 -21
  61. package/board/public/ui-snapshot.js +30 -13
  62. package/board/public/ui-threads.js +38 -23
  63. package/board/public/ui.js +96 -16
  64. package/board/public/view-frame.js +1 -1
  65. package/board/public/view-hub-settings.js +67 -44
  66. package/board/public/view-hub.js +105 -13
  67. package/board/public/view-notes.js +8 -5
  68. package/board/public/view-questions.js +123 -52
  69. package/board/public/view-settings.js +235 -62
  70. package/board/review.mjs +1 -0
  71. package/board/server.mjs +18 -7
  72. package/board/store.mjs +15 -6
  73. package/board/tickets.mjs +3 -3
  74. package/board/worktree.mjs +20 -13
  75. package/docs/hub-session.md +4 -4
  76. package/hooks-plugin/hooks/codex-share-auto.mjs +7 -5
  77. package/hooks-plugin/hooks/watcher-stop.mjs +41 -0
  78. package/hub/awake.mjs +1 -0
  79. package/hub/board-repo.mjs +1 -1
  80. package/hub/boards.mjs +4 -4
  81. package/hub/close-board.mjs +1 -1
  82. package/hub/duration.mjs +1 -0
  83. package/hub/github.mjs +3 -4
  84. package/hub/home.mjs +1 -1
  85. package/hub/hub.mjs +12 -10
  86. package/hub/incoming.mjs +2 -0
  87. package/hub/linear.mjs +2 -2
  88. package/hub/live.mjs +2 -2
  89. package/hub/new-board.mjs +2 -2
  90. package/hub/permission-denied-hook.mjs +2 -0
  91. package/hub/pr-report.mjs +3 -0
  92. package/hub/presence.mjs +1 -1
  93. package/hub/prompts.mjs +1 -1
  94. package/hub/push.mjs +2 -2
  95. package/hub/readiness.mjs +2 -0
  96. package/hub/repos.mjs +3 -2
  97. package/hub/restart-board.mjs +1 -0
  98. package/hub/routes.mjs +1 -1
  99. package/hub/schedule.mjs +1 -1
  100. package/hub/sessions.mjs +2 -2
  101. package/hub/shell.mjs +2 -1
  102. package/hub/slack.mjs +2 -0
  103. package/hub/stream.mjs +12 -12
  104. package/hub/sw.js +2 -2
  105. package/hub/trust.mjs +2 -0
  106. package/hub/try.mjs +8 -3
  107. package/hub/upgrade.mjs +1 -0
  108. package/hub/usage.mjs +3 -3
  109. package/hub/views.mjs +1 -1
  110. package/package.json +5 -1
  111. package/shared/card-prs.mjs +1 -0
  112. package/shared/colors.css +9 -6
  113. package/shared/fake-gh.mjs +105 -0
  114. package/shared/repos.mjs +1 -1
  115. package/shared/service.mjs +16 -16
  116. package/shared/skills.mjs +2 -0
  117. package/shared/usage-gate.mjs +1 -1
  118. package/starter/public/claude.js +3 -2
  119. package/bin/web-ts.mjs +0 -2
  120. package/board/public/view-polling.js +0 -87
package/hub/hub.mjs CHANGED
@@ -1,17 +1,19 @@
1
1
  #!/usr/bin/env node
2
- var ut=Object.defineProperty;var u=(e,t)=>ut(e,"name",{value:t,configurable:!0});import Se from"node:http";import $e from"node:net";import{spawn as lt,execFileSync as dt}from"node:child_process";import b from"node:fs";import S from"node:path";import xe from"node:os";import{fileURLToPath as ht}from"node:url";import ve from"node:vm";import ft from"node:zlib";import{routeRecord as T}from"./routes.mjs";import{publicKey as pt,addSubscription as mt,sendAll as gt,flushQueue as _e,queueStatus as ee,saveSettings as wt,readSettings as D,migrateSettings as bt,setNotifications as yt,readCodexShare as ke,setCodexShare as St,deliver as $t,pushRoute as xt}from"./push.mjs";import{POLL_MS as Te,answerOf as vt,expired as _t,pendingFor as je,promptRecord as kt,pushOf as Tt,updatePrompts as H,shown as jt,pruneRemembered as te,rememberRules as Ot,rememberedAnswer as Pt,rememberedFor as Oe}from"./prompts.mjs";import{nextSlot as Rt,windowAt as Et,hold as Dt,heldUntil as Ht,holdLine as Lt,editPolling as Ct}from"./schedule.mjs";import{capital as It,hubView as At,settingsView as Mt,pollingView as Bt,flushView as Ft,SNOOZE_REFUSED as Nt}from"./views.mjs";import{checkGitHub as Ut,sendWakes as Gt}from"./github.mjs";import{listBoards as O,isBoardSite as zt,listenState as Jt,RESUME_STATE as Wt,sendToBoards as Pe,checkListening as Kt,recordHubCode as Vt,runningHubCode as Zt,migrateHubFlag as Xt,recordLinkFaults as Qt,linksOf as Yt,hubLink as qt,hubCodeState as en,readJSON as tn}from"./boards.mjs";import{liveSessions as Re}from"./live.mjs";import{RuleError as nn,applyRules as sn,changed as Ee,readHubRules as ne,writeHubRules as an}from"./rules.mjs";import{siteIcon as on}from"./icons.mjs";import{readNeeds as rn,sidebarSites as cn,startRoute as un,hubHealth as ln}from"./home.mjs";import{checkSessions as dn,startingBoards as hn}from"./sessions.mjs";import{repoService as fn,repoLookupRoute as pn}from"./repos.mjs";import{boardRepo as mn,changeBoardRepo as gn,migrateBoardRepos as wn}from"./board-repo.mjs";import{requestBoard as bn,dismissBoard as yn}from"./new-board.mjs";import{boardClosure as Sn,closeBoard as $n,closedBoards as xn,reopenBoard as vn}from"./close-board.mjs";import{crossSite as _n,isJSON as De}from"../starter/cross-site.mjs";import{BOARD_PUBLIC as se,appFile as kn,colorsVersion as He,destOf as Tn,fileVersion as jn,pageOf as Le,shellHtml as On}from"./shell.mjs";import{hubStream as Pn}from"./stream.mjs";import{usageReader as Rn,checkUsagePause as En}from"./usage.mjs";import{readUsageGate as Ce,setPauseAt as Dn,setKeepGoing as Hn}from"../shared/usage-gate.mjs";import{codexStatusReader as Ln}from"../shared/codex.mjs";import{BuildError as Cn,startBuild as In,stopTries as An,tryTick as Mn,servingTry as Ie,initializeTries as Bn,fetchTryBranches as Fn}from"./try.mjs";import{migrateOnStart as Nn}from"../bin/install.mjs";import{serveFiles as Un}from"./files.mjs";import{LinearError as ae,linearStatus as oe,removeLinearKey as Gn,saveLinearKey as zn,savedClient as Ae}from"./linear.mjs";const U=S.dirname(ht(import.meta.url)),Jn=S.join(U,"..","shared","colors.css"),h=u((...e)=>console.log(new Date().toISOString(),...e),"log"),Me=Nn();for(const e of Me.steps)h(`migration: ${e}`);for(const e of Me.conflicts)h(`migration failed: ${e}`);const G=S.join(xe.homedir(),".claude","kickflip"),Be=S.join(G,"manifest.json"),re=S.join(G,"logs"),Fe=S.join(G,"run"),ie=Number(process.env.KICKFLIP_HUB_PORT||4500);for(const e of[G,re,Fe])b.mkdirSync(e,{recursive:!0});try{const e=Vt();h(`hub code ${e.hash} (${e.files.length} files)`)}catch(e){h("could not record the hub code hash:",e.message)}try{const e=await Xt();e&&h(`manifest: flagged /${e} as the hub board`)}catch(e){h("hub board migration failed:",e.message)}try{const e=await wn();e.length&&h(`manifest: recorded GitHub repos for ${e.map(t=>`/${t}`).join(", ")}`)}catch(e){h("board repo migration failed:",e.message)}const L=fn(),Wn=Rn(),z=Ln();let J={version:1,sites:{}};function w(){try{J=JSON.parse(b.readFileSync(Be,"utf8")),J.sites||={}}catch(e){e.code==="ENOENT"?J={version:1,sites:{}}:h("manifest unreadable, keeping the last good copy:",e.message)}return J}u(w,"loadManifest");const x=new Map,R=new Set;let W=!1;const Ne=5e3,ce=u(e=>JSON.stringify([e.cmd,e.cwd||e.dir,e.port,e.env||{},e.restart_at||0]),"keyOf"),ue=u(e=>Array.isArray(e?.cmd)&&e.cmd.length>0&&!!e.port,"runs"),C=u(e=>ue(e)?"cmd":e?.port?"port":"files","servesOf"),I=new Map;function K(e){return S.join(Fe,e+".pid")}u(K,"pidFile");function Kn(e,t){try{const n=Number(b.readFileSync(K(e),"utf8")),o=n?dt("ps",["-o","command=","-p",String(n)],{encoding:"utf8"}).trim():"";o&&o.includes(t.cmd[t.cmd.length-1])&&(process.kill(-n,"SIGTERM"),h(`stopped stale ${e} (pgid ${n})`))}catch{}try{b.unlinkSync(K(e))}catch{}}u(Kn,"killStale");function Vn(e,t){const n=x.get(e)||{restarts:0},o=b.openSync(S.join(re,e+".log"),"a"),[s,...l]=t.cmd,c={...process.env,...t.env||{},PORT:String(t.port),PREFIX:"/"+e,KICKFLIP_ROUTE:e};b.writeSync(o,`
2
+ var Et=Object.defineProperty;var u=(e,t)=>Et(e,"name",{value:t,configurable:!0});import Re from"node:http";import Ee from"node:net";import{spawn as De,execFileSync as Dt}from"node:child_process";import w from"node:fs";import $ from"node:path";import Le from"node:os";import{fileURLToPath as Lt}from"node:url";import Ce from"node:vm";import Ct from"node:zlib";import{routeRecord as j}from"./routes.mjs";import{publicKey as Ht,addSubscription as At,removeSubscription as It,sendAll as Mt,flushQueue as He,queueStatus as Ae,saveSettings as Bt,readSettings as V,migrateSettings as Ft,setNotifications as Nt,readCodexShare as Ie,setCodexShare as Ut,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 Me,answerOf as Vt,expired as Zt,pendingFor as Be,promptRecord as Qt,pushOf as Xt,updatePrompts as M,shown as Yt,pruneRemembered as he,rememberRules as qt,rememberedAnswer as en,rememberedFor as Fe}from"./prompts.mjs";import{hold as tn,heldUntil as nn,holdLine as an,editQuiet as rn,checkEvery as Ne,nextCheckAt as sn,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,isBoardSite as Ge,listenState as ze,RESUME_STATE as Sn,sendToBoards as We,checkListening as kn,recordHubCode as $n,runningHubCode as _n,boardCode as Je,recordLinkFaults as xn,linksOf as Tn,hubLink as vn,hubCodeState as On,readJSON as jn}from"./boards.mjs";import{liveSessions as Ke}from"./live.mjs";import{RuleError as Pn,applyRules as Rn,changed as Ve,readHubRules as fe,writeHubRules as En}from"./rules.mjs";import{siteIcon as Dn}from"./icons.mjs";import{readNeeds as Ln,sidebarSites as Cn,startRoute as Hn,hubHealth as An}from"./home.mjs";import{checkSessions as In,compactFullContexts as Mn,startingBoards as Bn}from"./sessions.mjs";import{repoService as Fn,repoLookupRoute as Nn}from"./repos.mjs";import{boardRepo as Ze,changeBoardRepo as Un,migrateBoardRepos as Gn,migrateHubBoard as zn}from"./board-repo.mjs";import{requestBoard as Wn,retryBoard as Jn,dismissBoard as Kn,clonePercent as Vn}from"./new-board.mjs";import{boardClosure as Zn,closeBoard as Qn,closedBoards as Xn,reopenBoard as Yn}from"./close-board.mjs";import{restartBoard as qn,restartable as ea}from"./restart-board.mjs";import{crossSite as ta,isJSON as pe}from"../starter/cross-site.mjs";import{BOARD_PUBLIC as me,appFile as na,colorsVersion as Qe,destOf as aa,fileVersion as ra,pageOf as Xe,shellHtml as sa}from"./shell.mjs";import{hubStream as oa}from"./stream.mjs";import{usageReader as ia,checkUsagePause as ca,contextReading as ua}from"./usage.mjs";import{readUsageGate as Ye,setPauseAt as la,setKeepGoing as da}from"../shared/usage-gate.mjs";import{readiness as ha,readCodex as Z}from"./readiness.mjs";import{BuildError as fa,startBuild as pa,stopTries as ma,tryTick as ge,initializeTries as ga,fetchTryBranches as qe,pullNow as wa,pullMains as ba}from"./try.mjs";import{serveFiles as ya}from"./files.mjs";import{upgrader as Sa,CHECK_MS as ka}from"./upgrade.mjs";import{fallBackFromLinear as $a,openBoardBuilders as et}from"../board/store.mjs";import{recordRefusal as _a}from"../board/permissions.mjs";import{fromOwner as tt}from"../board/on-host.mjs";import{restartRunner as xa}from"../board/worktree.mjs";import{hubService as Ta}from"../shared/service.mjs";import{cloneRoot as nt}from"../shared/repos.mjs";import{LinearError as we,linearStatus as Q,removeLinearKey as va,saveLinearKey as Oa,savedClient as at}from"./linear.mjs";const X=$.dirname(Lt(import.meta.url)),ja=$.join(X,"..","shared","colors.css"),c=u((...e)=>console.log(new Date().toISOString(),...e),"log"),Y=$.join(Le.homedir(),".claude","kickflip"),rt=$.join(Y,"manifest.json"),q=$.join(Y,"logs"),st=$.join(Y,"run"),be=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 Gn();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 zn();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=Fn(),Pa=ia(),ee=pn({log:c});ee.apply();let te={version:1,sites:{}};function g(){try{te=JSON.parse(w.readFileSync(rt,"utf8")),te.sites||={}}catch(e){e.code==="ENOENT"?te={version:1,sites:{}}:c("manifest unreadable, keeping the last good copy:",e.message)}return te}u(g,"loadManifest");const O=new Map,C=new Set;let ne=!1;const ot=5e3,ae=u(e=>JSON.stringify([e.cmd,e.cwd||e.dir,e.port,e.env||{},e.restart_at||0]),"keyOf"),ye=u(e=>Array.isArray(e?.cmd)&&e.cmd.length>0&&!!e.port,"runs"),B=u(e=>ye(e)?"cmd":e?.port?"port":"files","servesOf"),F=new Map;function re(e){return $.join(st,e+".pid")}u(re,"pidFile");function Ra(e,t){try{const n=Number(w.readFileSync(re(e),"utf8")),s=n?Dt("ps",["-o","command=","-p",String(n)],{encoding:"utf8"}).trim():"";s&&s.includes(t.cmd[t.cmd.length-1])&&(process.kill(-n,"SIGTERM"),c(`stopped stale ${e} (pgid ${n})`))}catch{}try{w.unlinkSync(re(e))}catch{}}u(Ra,"killStale");function Ea(e,t){const n=O.get(e)||{restarts:0},s=w.openSync($.join(q,e+".log"),"a"),[a,...l]=t.cmd,h={...process.env,...t.env||{},PORT:String(t.port),PREFIX:"/"+e,KICKFLIP_ROUTE:e};w.writeSync(s,`
3
3
  --- ${new Date().toISOString()} hub starting: ${t.cmd.join(" ")} (port ${t.port})
4
- `);let f;try{f=lt(s,l,{cwd:t.cwd||t.dir,env:c,stdio:["ignore",o,o],detached:!0})}catch(y){b.writeSync(o,`hub could not start it: ${y.message}
5
- `),b.closeSync(o),Object.assign(n,{child:null,key:ce(t),state:"crashed",exitCode:null,nextStart:Date.now()+Ue(n.restarts++)}),x.set(e,n);return}b.closeSync(o),b.writeFileSync(K(e),String(f.pid)),Object.assign(n,{child:f,key:ce(t),state:"starting",since:Date.now(),exitCode:null}),x.set(e,n),h(`started ${e} pid ${f.pid}`),f.pid&&R.add(f);const g=u((y,m)=>{if(R.delete(f),n.child!==f)return;try{b.unlinkSync(K(e))}catch{}Date.now()-n.since>6e4&&(n.restarts=0),Object.assign(n,{child:null,state:n.stopping?"stopped":"crashed",exitCode:y??m,nextStart:Date.now()+Ue(n.restarts++)}),n.stopping=!1,h(`${e} exited (${y??m})`)},"ended");f.on("error",y=>{h(`${e} spawn error: ${y.message}`),f.pid||g(y.code,null)}),f.on("exit",g)}u(Vn,"start");const Ue=u(e=>Math.min(6e4,1e3*2**Math.min(e,6)),"backoff");function le(e){const t=x.get(e);if(!t||!t.child){x.delete(e);return}t.stopping=!0;const n=t.child;V(n,"SIGTERM"),setTimeout(()=>{R.has(n)&&V(n,"SIGKILL")},Ne)}u(le,"stop");const V=u((e,t)=>{try{process.kill(-e.pid,t)}catch{}},"signal");function Ge(e){return new Promise(t=>{const n=$e.connect({port:e,host:"127.0.0.1"}),o=u(s=>{n.destroy(),t(s)},"done");n.setTimeout(800,()=>o(!1)),n.on("connect",()=>o(!0)),n.on("error",()=>o(!1))})}u(Ge,"probe");let ze=!0;const A=new Set;async function Je(){if(W)return;const{sites:e}=w();for(const t of[...x.keys()])A.has(t)||(T(e,t)?ue(T(e,t))||(h(`${t} no longer has a command; stopping it`),le(t),x.delete(t)):(h(`${t} left the manifest; stopping it`),le(t),x.delete(t)));for(const t of[...I.keys()])C(T(e,t))!=="port"&&I.delete(t);for(const[t,n]of Object.entries(e))C(n)==="port"&&I.set(t,await Ge(n.port));for(const[t,n]of Object.entries(e)){if(!ue(n))continue;ze&&Kn(t,n);const o=x.get(t);if(o&&o.child&&o.key!==ce(n)){h(`${t} entry changed; restarting it`),le(t),o.restarts=0,o.nextStart=Date.now()+1500;continue}(!o||!o.child&&Date.now()>=(o.nextStart||0))&&Vn(t,n)}ze=!1;for(const[t,n]of x){const o=T(e,t);o&&(n.up=n.child?await Ge(o.port):!1,n.child&&n.up&&(n.state="up"))}}u(Je,"reconcile");const Zn=setInterval(()=>Je().catch(e=>h("reconcile failed:",e.message)),2e3);Je().catch(e=>h("reconcile failed:",e.message));try{bt()&&h("settings: migrated the push schedule to the polling interval")}catch(e){h("settings migration failed:",e.message)}const Xn=6e4,Qn=60*6e4,$={running:null,startedAt:0,firstAt:Date.now()+Xn,last:null,errorLoggedAt:0},We=u(()=>new Date($.startedAt?Rt(new Date($.startedAt),D()).getTime():$.firstAt),"nextPollAt");function Ke({flush:e=!0}={}){if($.running)return $.running;const t=new Date;return $.startedAt=t.getTime(),$.running=(async()=>{const n=D();let o;try{o=await Ut({every:Et(t,n).every_minutes,manifest:w()})}catch(s){o={at:t.toISOString(),watched:0,failed:[s.message],changes:[],events:[],checked:[]}}await Fn({manifest:w(),log:h}),o.failed.length&&Date.now()-$.errorLoggedAt>Qn&&($.errorLoggedAt=Date.now(),h(`github check: ${o.failed.length} step${o.failed.length===1?"":"s"} failed, first: ${o.failed[0]} (logged at most hourly)`)),o.changes.length&&h(`github check: ${o.changes.map(s=>`#${s.pr} ${s.changes.join(",")}`).join("; ")} \u2192 ${o.events.map(s=>s.route).join(", ")||"no board"}`);try{o.deaf=await Kt({manifest:w()})}catch(s){o.deaf={pushed:[],failed:[s.message]}}(o.deaf.pushed.length||o.deaf.failed.length)&&h(`not listening: pushed ${o.deaf.pushed.join(", ")||"none"}${o.deaf.failed.length?`; ${o.deaf.failed.join("; ")}`:""}`);try{Qt(w())}catch(s){h("could not record link faults:",s.message)}if(o.flushed=null,o.held=Ht(t,n)!==null,e&&!o.held)try{const s=await _e();s&&(o.flushed={count:s.count,sent:s.sent,ok:s.ok},h(`push queue: sent ${s.sent} of ${s.count} queued`))}catch(s){h("push queue flush failed:",s.message)}return $.last=o,o})().finally(()=>{$.running=null}),$.running}u(Ke,"runPoll");let Ve=null;function Yn(){Ve||=Gt({settings:D(),manifest:w()}).then(e=>{e.due.length&&h(`wake ${e.due.join(", ")}: check-github \u2192 ${e.events.map(t=>t.route).join(", ")||"no board"}${e.failed.length?`; ${e.failed.join("; ")}`:""}`)}).catch(e=>h("wake failed:",e.message)).finally(()=>{Ve=null})}u(Yn,"runWakes");const Z={running:null};function qn(){Z.running||A.size||(Z.running=dn({manifest:w()}).then(e=>{(e.resumed.length||e.pushed.length||e.failed.length)&&h(`sessions: resumed ${e.resumed.join(", ")||"none"}; pushed ${e.pushed.join(", ")||"none"}${e.failed.length?`; ${e.failed.join("; ")}`:""}`)}).catch(e=>h("session check failed:",e.message)).finally(()=>{Z.running=null}))}u(qn,"runSessionCheck"),Bn(w());const es=Date.now(),M=new Map;function B(e){e?(X.delete(e),v.changed(e)):v.refresh()}u(B,"promptChanged");async function Ze(){const{gone:e,rulesChanged:t}=await H(n=>{const o=_t(n,{now:Date.now(),aliveSince:es}),s=te(n,de());for(const l of o)delete n.prompts[l.id];return{gone:o,rulesChanged:s}});for(const n of e)B(n.route);t&&v.refresh(),e.length&&h(`prompts: dropped ${e.map(n=>n.id).join(", ")}, whose hook went away`)}u(Ze,"prunePrompts");const de=u(()=>({running:Re(),boards:O(w())}),"promptContext");let he=!1;async function Xe(){if(!he){he=!0;try{await En({gate:Ce(),resume:u(async e=>{const t=await Pe(O(w()),"usage-resumed",{window:e});for(const n of t.failed)h(n)},"resume")})}finally{he=!1}}}u(Xe,"checkUsageGate"),setInterval(()=>{Mn({manifest:w()}).catch(e=>h("try tick failed:",e.message)),Xe().catch(e=>h("usage gate failed:",e.message)),!$.running&&Date.now()>=We().getTime()&&Ke().catch(e=>h("poll failed:",e.message)),Yn(),Ze().catch(e=>h("prompt expiry failed:",e.message)).finally(qn)},15e3);function Qe(){if(W)return;W=!0,clearInterval(Zn),h("hub stopping; stopping sites");for(const n of x.values())n.child&&(n.stopping=!0);for(const n of R)V(n,"SIGTERM");let e=!1;An().finally(()=>{e=!0});const t=Date.now()+Ne;setInterval(()=>{if(!((!e||R.size)&&Date.now()<t)){for(const n of R)h(`pid ${n.pid} ignored SIGTERM; killing it`),V(n,"SIGKILL");process.exit(0)}},50)}u(Qe,"shutdown"),process.on("SIGTERM",Qe),process.on("SIGINT",Qe);const Ye=new Map;function qe(e,t={n:3e3}){let n=0,o;try{o=b.readdirSync(e,{withFileTypes:!0})}catch{return 0}for(const s of o){if(t.n--<=0)break;if(s.name.startsWith(".")||s.name==="node_modules")continue;const l=S.join(e,s.name);if(s.isDirectory())n=Math.max(n,qe(l,t));else try{n=Math.max(n,b.statSync(l).mtimeMs)}catch{}}return n}u(qe,"newestMtime");function ts(e,t){const n=Ye.get(e);if(n&&Date.now()-n.checked<3e4)return n.at;const o=qe(t.dir);return Ye.set(e,{at:o,checked:Date.now()}),o}u(ts,"changedAt");const et=u(e=>{try{return typeof e=="string"&&b.statSync(e).isDirectory()}catch{return!1}},"isFolder");function fe(){const{sites:e}=w();return Object.entries(e).sort(([t],[n])=>t.localeCompare(n)).map(([t,n])=>{const o=x.get(t)||{},s=C(n),l=s==="files"?et(n.dir)?"up":"down":s==="port"?I.has(t)?I.get(t)?"up":"down":"starting":o.child?o.up?"up":"starting":o.state==="crashed"?"down":o.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:s,state:l,exitCode:o.exitCode??null,owner:n.owner||null,changedAt:ts(t,n)||null,created:n.created||null,log:S.join(re,t+".log")}})}u(fe,"status");const j=u(e=>String(e??"").replace(/[&<>"']/g,t=>({"&":"&amp;","<":"&lt;",">":"&gt;",'"':"&quot;","'":"&#39;"})[t]),"esc"),X=new Map,tt=2e4,ns=1500;async function ss(e){try{const t=await fetch(`http://127.0.0.1:${e}/_claude/needs`,{signal:AbortSignal.timeout(ns)});return t.ok?rn(await t.json()):null}catch{return null}}u(ss,"fetchNeeds");async function nt(e,{fresh:t=!1}={}){const n=e.filter(s=>s.state==="up"&&s.owner&&s.port);for(const s of n){const l=X.get(s.route);(t===!0||t?.has?.(s.route)||!l||Date.now()-l.at>=tt)&&X.set(s.route,{at:Date.now(),needs:ss(s.port)})}const o=await Promise.all(n.map(s=>X.get(s.route).needs));return new Map(n.map((s,l)=>[s.route,o[l]]))}u(nt,"waitingCounts");const st=u(e=>[...e.values()].reduce((t,n)=>t+(n?.count||0),0)+je(null).length,"badgeTotal"),F={at:0,value:null};function as(e){if(F.value&&Date.now()-F.at<tt)return F.value;const t=new Map,n=new Map,o={running:Re(),wakes:tn(Wt,{})};for(const s of e)try{const l=Jt(s,o);n.set(s.route,l),t.set(s.route,Yt(s,{listen:l}))}catch(l){h(`links of ${s.route} failed:`,l.message),t.set(s.route,[])}return Object.assign(F,{at:Date.now(),value:{links:t,listens:n}}),F.value}u(as,"boardLinks");async function Q({fresh:e=!1}={}){const t=new Date,n=fe(),o=await nt(n,{fresh:e}),s=w(),l=O(s),{links:c,listens:f}=as(l),g=en(),y=cn(n,o,new Set(l.map(r=>r.route)),f),m=D(),p=Dt(t,m);return{data:{code:g.running??null,total:st(o),start:un(y),sites:y,hub:ln(y,c,qt(g)),home:b.realpathSync(xe.homedir()),starting:hn(s,t.getTime()),usage:Wn({codexShare:ke(z())}),pause:Ce(),notifications:p?p.why==="off"?{state:"off",text:"Off"}:{state:"held",text:It(Lt(p,t))}:{state:"on",text:"On"}},links:c,rows:n,code:g}}u(Q,"sidebarState");function at(e,t,n,o,s){let l=Buffer.isBuffer(s)?s:Buffer.from(s);const c={...o,vary:[o.vary,"accept-encoding"].filter(Boolean).join(", ")};l.length>=1024&&/\bgzip\b/.test(e.headers["accept-encoding"]||"")&&(l=ft.gzipSync(l),c["content-encoding"]="gzip"),c["content-length"]=l.length,t.writeHead(n,c),t.end(e.method==="HEAD"?void 0:l)}u(at,"sendText");async function os(e){const{data:t}=await Q(),n=ve.createContext({});for(const o of["ui-page-header.js","ui-sidebar.js"])ve.runInContext(b.readFileSync(S.join(se,o),"utf8"),n);return On({sidebar:n.BoardUI.sidebar(t,{here:e}),sidebarData:JSON.stringify(t).replace(/</g,"\\u003c"),touchIcon:Y(180)})}u(os,"shellPage");const v=Pn({targets:u(()=>Object.entries(w().sites).filter(([,e])=>zt(e)&&e.port).map(([e,t])=>({route:e,port:t.port})),"targets"),sidebar:u(async({fresh:e})=>(await Q({fresh:e})).data,"sidebar"),log:h});function Y(e){let t="0";try{t=Math.floor(b.statSync(S.join(U,`icon-${e}.png`)).mtimeMs).toString(36)}catch{}return`/_hub/icon-${e}.png?v=${t}`}u(Y,"iconUrl");const rs=u(()=>JSON.stringify({name:"Claude sites",short_name:"Claude",start_url:"/",scope:"/",display:"standalone",background_color:"#121514",theme_color:"#121514",icons:[{src:Y(192),sizes:"192x192",type:"image/png"},{src:Y(512),sizes:"512x512",type:"image/png"}]}),"manifestJson");async function E(e,t=16*1024){let n="";for await(const o of e)if(n+=o,n.length>t)throw Object.assign(new Error("too large"),{status:413});return n}u(E,"readBody");async function k(e,t){if(!De(e.headers))throw Object.assign(new Error("Send the body as application/json."),{status:403});const n=await E(e,t);return n?JSON.parse(n):{}}u(k,"readJSONBody");const pe=u((e,t="")=>`<!doctype html><html lang="en" class="hub-view"><head><meta charset="utf-8"><meta name="viewport" content="width=device-width, initial-scale=1, viewport-fit=cover">
6
- <title>${j(e)}</title><link rel="manifest" href="/manifest.webmanifest"><link rel="icon" type="image/png" href="/_hub/kickflip.png"><link rel="apple-touch-icon" href="${Y(180)}">
7
- <meta name="apple-mobile-web-app-title" content="Claude">${t}<link rel="stylesheet" href="/_hub/colors.css?v=${He()}"><link rel="stylesheet" href="/_hub/hub.css"></head><body><main>`,"head"),is=u(e=>/\bapplication\/json\b/.test(e.headers.accept||""),"wantsJSON"),ot=u(()=>({running:!!$.running,nextAt:We(),last:$.last}),"pollState");async function me(e){await Ze();const t=new Date,{data:n,links:o,rows:s,code:l}=await Q();return{...At({now:t,st:ee(t),poll:ot(),sidebar:n,rows:s,links:o,rules:ne(),code:l,host:e.headers["x-forwarded-host"]||e.headers.host||"",codexShare:ke(z()),codex:z(),remembered:new Map(O(w()).map(c=>[c.route,Oe(c.route)]))}),prompts:je(null).map(jt),remembered:Oe(null)}}u(me,"hubData");const ge=u(async()=>{const e=new Date;return{...Mt({now:e,st:ee(e),rules:ne(),route:await xt(),linear:oe()}),closed:xn()}},"settingsData"),rt=u(e=>{const t=new Date;return Bt({now:t,st:ee(t),poll:ot(),select:e})},"pollingData"),P=u((e,t)=>{e.writeHead(303,{location:t,"cache-control":"no-store"}),e.end()},"redirect");async function cs(e,t,n){const o=u((a,r,i,d={})=>{t.writeHead(a,{"content-type":r,"cache-control":"no-store",...d}),t.end(i)},"send"),s=u((a,r)=>{const i=Zt();return o(a,"application/json",JSON.stringify(r),i?{"x-hub-code":i}:{})},"sendJSON"),l=is(e),c=n.pathname,f=/^\/_hub\/close-board\/([a-z0-9-]+)$/.exec(c);if(f&&e.method==="GET")try{return s(200,await Sn(f[1]))}catch(a){return s(a.status||400,{error:a.message})}if(["/_hub/close-board","/_hub/reopen-board"].includes(c)&&e.method==="POST"){let a;try{const r=await k(e);if(a=r.route,c==="/_hub/reopen-board"){const _=await vn(a);return v.refresh(),s(200,_)}if(A.has(a))return s(409,{error:"That board is already closing."});A.add(a);const i=u(()=>A.delete(a),"release");t.once("finish",i),t.once("close",i),await Z.running;const d=await $n(a,r.name);return v.refresh(),s(200,d)}catch(r){return s(r.status||400,{error:r.message})}}if(c==="/_hub/repos"&&e.method==="GET")try{return s(200,await L.list(w(),{fresh:n.searchParams.get("fresh")==="1"}))}catch(a){return s(a.status||502,{error:a.message})}if(c==="/_hub/branches"&&e.method==="GET")try{return s(200,await L.branches(n.searchParams.get("github")))}catch(a){return s(a.status||502,{error:a.message})}if(c==="/_hub/repo"&&e.method==="GET")return pn(n,{repos:L,manifest:w(),sendJSON:s});const g=/^\/_hub\/board-repo\/([a-z0-9-]+)$/.exec(c);if(g&&["GET","POST"].includes(e.method))try{const a=e.method==="GET"?mn(g[1]):await gn(g[1],await k(e),{repos:L});return e.method==="POST"&&v.refresh(),s(200,a)}catch(a){return s(a.status||400,{error:a.message})}if(c==="/_hub/new-board"&&e.method==="POST"){if(!De(e.headers))return o(403,"text/plain; charset=utf-8","Send name, github, branch and purpose as application/json.");let a;try{a=await k(e)}catch{return o(400,"text/plain; charset=utf-8","Send name, github, branch and purpose as JSON.")}try{const{route:r,started:i}=await bn(a,{repos:L,changed:u(()=>v.refresh(),"changed")});return i.then(({error:d})=>{d&&h(`new board /${r} didn't start: ${d}`),v.refresh()}),v.refresh(),s(200,{route:r})}catch(r){if(r.status)return o(r.status,"text/plain; charset=utf-8",r.message);throw r}}const y=/^\/_hub\/new-board\/([a-z0-9-]+)$/.exec(c);if(y&&e.method==="DELETE")try{const a=await yn(y[1]);return v.refresh(),s(200,a)}catch(a){if(a.status)return o(a.status,"text/plain; charset=utf-8",a.message);throw a}if(c==="/_hub/build"&&e.method==="POST"){if(W)return s(503,{error:"The hub is stopping. Try again after it restarts."});const a=["127.0.0.1","::1","::ffff:127.0.0.1"],r=(e.headers.host||"").replace(/:\d+$/,"");if(!a.includes(e.socket.remoteAddress)||!["127.0.0.1","localhost","[::1]"].includes(r)||e.headers.origin||Object.keys(e.headers).some(d=>d==="forwarded"||d.startsWith("x-forwarded-")))return s(403,{error:"Builds can only be started from the host over loopback."});let i;try{i=await k(e)}catch(d){return s(d.status||400,{error:"Send the board route and card key as JSON."})}try{return s(202,await In(i,{manifest:w(),log:h}))}catch(d){if(d instanceof Cn)return s(d.status,{error:d.message});throw d}}if(c==="/sw.js")return o(200,"text/javascript; charset=utf-8",b.readFileSync(S.join(U,"sw.js")),{"service-worker-allowed":"/"});if(c==="/manifest.webmanifest")return o(200,"application/manifest+json",rs());const m=/^\/_hub\/icon-(180|192|512)\.png$/.exec(c);if(m)try{return o(200,"image/png",b.readFileSync(S.join(U,`icon-${m[1]}.png`)),{"cache-control":"max-age=86400"})}catch{t.writeHead(404).end();return}if(c==="/_hub/colors.css"){const a=n.searchParams.get("v")===He()?"public, max-age=31536000, immutable":"no-cache";try{return o(200,"text/css; charset=utf-8",b.readFileSync(Jn),{"cache-control":a})}catch{t.writeHead(404).end();return}}if(c==="/_hub/kickflip.png")try{return o(200,"image/png",b.readFileSync(S.join(se,"kickflip.png")),{"cache-control":"max-age=86400"})}catch{t.writeHead(404).end();return}if(c==="/_hub/hub.css")return o(200,"text/css; charset=utf-8",b.readFileSync(S.join(se,"hub.css")));if(c==="/_hub/sidebar.json")return s(200,(await Q({fresh:n.searchParams.get("fresh")==="1"})).data);if(c==="/_hub/stream"&&e.method==="GET"){v.handle(e,t);return}if(c.startsWith("/_hub/app/")){const a=kn(c.slice(10));if(!a){t.writeHead(404).end();return}const r=S.extname(a),i={".js":"text/javascript; charset=utf-8",".css":"text/css; charset=utf-8",".png":"image/png"}[r],d=n.searchParams.get("v")===jn(a)?"public, max-age=31536000, immutable":"no-cache";return r===".png"?o(200,i,b.readFileSync(a),{"cache-control":d}):at(e,t,200,{"content-type":i,"cache-control":d},b.readFileSync(a))}if(c==="/_hub/api/hub")return s(200,await me(e));if(c==="/_hub/api/settings")return s(200,await ge());if(c==="/_hub/api/polling")return s(200,rt(n.searchParams.get("w")));if(c==="/_hub/push/key")return s(200,{key:pt()});if(c==="/_hub/push/subscribe"&&e.method==="POST"){const a=e.headers["x-forwarded-host"]||e.headers.host;return s(200,{count:mt(await k(e),`https://${a}/`,e.headers["user-agent"])})}if(c==="/_hub/push/test"&&e.method==="POST"){const a=await gt({title:"Claude",body:"Test notification from the hub.",url:"/",tag:"hub-test"});return s(200,{sent:a.filter(r=>r.status>=200&&r.status<300).length,results:a})}if(c==="/_hub/settings/notifications"&&e.method==="POST"){const a=new URLSearchParams(await E(e)),r=a.get("set"),i=r==="on"?{on:a.get("value")==="on"}:r==="weekends"?{weekends_off:a.get("value")==="on"}:a.has("snooze")?{snooze:a.get("snooze")}:null,d="Pass set=on or set=weekends with value=on, or snooze.";if(!i)return l?s(400,{error:d}):o(400,"text/plain; charset=utf-8",d);let _;try{_=await yt(i)}catch(N){if(N.status!==400)throw N;return l?s(400,{error:Nt}):P(t,"/_hub/?refused=snooze#notifications")}return _.dropped&&h(`notifications off: dropped ${_.dropped} queued push${_.dropped===1?"":"es"}`),l?s(200,await ge()):P(t,"/_hub/#notifications")}if(c==="/_hub/settings/codex"&&e.method==="POST"){const a=new URLSearchParams(await E(e));try{St(a.get("value"),z({fresh:!0}))}catch(r){if(r.status!==400&&r.status!==409)throw r;return l?s(r.status,{error:r.message}):o(r.status,"text/plain; charset=utf-8",r.message)}return l?s(200,await me(e)):P(t,"/_hub/")}if(["/_hub/settings/pause-at","/_hub/settings/keep-going"].includes(c)&&e.method==="POST"){const a=new URLSearchParams(await E(e));try{c.endsWith("/pause-at")?Dn(a.get("value")):Hn(a.get("value")),await Xe()}catch(r){if(r.status!==400&&r.status!==409)throw r;return l?s(r.status,{error:r.message}):o(r.status,"text/plain; charset=utf-8",r.message)}return l?s(200,await me(e)):P(t,"/_hub/")}if(c==="/_hub/settings/linear"&&e.method==="POST"){const a=new URLSearchParams(await E(e,4096));try{a.has("remove")?Gn():await zn(a.get("key"))}catch(r){if(!(r instanceof ae))throw r;return l?s(r.status,{error:r.message}):o(r.status,"text/plain; charset=utf-8",r.message)}return h(`linear: key ${a.has("remove")?"removed":"saved"}`),l?s(200,await ge()):P(t,"/_hub/#linear")}if(c==="/_hub/api/linear"&&e.method==="GET"){const a=Ae();if(!a)return s(200,{set:!1});try{const r=await a.teams();return s(200,{...oe(),teams:r.map(({key:i,name:d})=>({key:i,name:d})),projects:r.flatMap(i=>i.projects.map(d=>({id:d.id,name:d.name,team:i.key})))})}catch(r){if(!(r instanceof ae))throw r;return s(200,{...oe(),error:r.message})}}if(c==="/_hub/linear/projects"&&e.method==="POST"){const a=Ae();if(!a)return s(400,{error:"Add a Linear key on Hub first."});let r;try{r=await k(e,4*1024)}catch(d){return s(d.status||400,{error:"Send the team and project name as JSON."})}const i=typeof r.name=="string"?r.name.trim():"";if(!i||i.length>80||typeof r.team!="string"||!/^[A-Z][A-Z0-9]*$/.test(r.team))return s(400,{error:"Name the project and its team."});try{return s(200,await a.createProject(r.team,i))}catch(d){if(!(d instanceof ae))throw d;return s(d.status,{error:d.message})}}if(c==="/_hub/settings/polling"&&e.method==="POST"){const a=new URLSearchParams(await E(e)),r=a.get("op")||"",i=Ct(D(),{op:r,w:a.get("w")||"",value:(a.get("value")||"").trim()});try{if(i.error)throw Object.assign(new Error(i.error),{status:400});wt(i.input)}catch(d){if(d.status!==400)throw d;return l?s(400,{error:d.message}):o(400,"text/plain; charset=utf-8",d.message)}return l?s(200,rt(i.select)):P(t,`/_hub/?w=${encodeURIComponent(i.select)}#polling`)}if(c==="/_hub/settings/rules"&&e.method==="POST"){let a,r;try{a=await k(e,64*1024),r=sn(ne(),a.rules,a.base??[]),Ee(r.changes)&&an(r.rules)}catch(d){if(!(d instanceof nn)&&!(d instanceof SyntaxError))throw d;return s(400,{error:d.message})}if(!Ee(r.changes))return s(200,{rules:r.rules,changed:!1,events:[],failed:[]});const i=await Pe(O(w()),"rules",{scope:"hub",...r.changes});return h(`hub rules saved: rules \u2192 ${i.events.map(d=>d.route).join(", ")||"no board"}${i.failed.length?`; ${i.failed.join("; ")}`:""}`),s(200,{rules:r.rules,changed:!0,...i})}if(c==="/_hub/push/flush"&&e.method==="POST"){const a=await _e();return a&&h(`push queue: ${a.ok?"sent":"could not send"} ${a.count} queued (from the settings page)`),l?s(200,Ft(a)):P(t,`/_hub/settings/polling?flushed=${a?a.ok?a.count:"failed":0}`)}if(c==="/_hub/poll"&&e.method==="POST"){const a=await k(e);return s(200,await Ke({flush:a.flush!==!1}))}if(c==="/_hub/prompts"&&e.method==="POST"){let a;try{a=await k(e,8*1024*1024)}catch(d){if(d.status)throw d;return s(400,{error:"Send the hook input as JSON."})}if(!a?.tool_name||a.hook_event_name&&a.hook_event_name!=="PermissionRequest")return s(400,{error:"Send a PermissionRequest hook input."});const{answer:r,rec:i}=await H(d=>{const _=de(),N=_.boards.find(ye=>ye.owner?.session&&ye.owner.session===a.session_id)||null;te(d,_)&&v.refresh();const be=Pt(d,a);if(be)return{answer:be};const q=kt(a,{board:N,live:_.running.get(a.session_id)});return d.prompts[q.id]=q,{rec:q}});return r?s(200,{state:"answered",answer:r}):(B(i.route),h(`prompt ${i.id}: ${i.route?`/${i.route}`:i.cwd} ${i.tool}${i.action?` (${i.action})`:""}`),$t(Tt(i),{kind:"prompt",immediate:!0}).catch(d=>h(`prompt ${i.id}: push failed: ${d.message}`)),s(200,{id:i.id,deadline:i.deadline}))}const p=/^\/_hub\/prompts\/([\w-]+)(\/wait|\/answer)?$/.exec(c);return p?us(e,t,{id:p[1],op:p[2]?.slice(1)||null,url:n,sendJSON:s}):!1}u(cs,"hubRoute");async function us(e,t,{id:n,op:o,url:s,sendJSON:l}){if(o==="wait"&&e.method==="GET"){const c=u(()=>H(p=>{const a=T(p.prompts,n);return a?.answer?delete p.prompts[n]:a&&(a.seen=new Date().toISOString()),a}),"seen"),f=u(p=>(B(p.route),l(200,{state:"answered",answer:p.answer})),"take"),g=await c();if(!g)return l(404,{state:"gone"});if(g.answer)return f(g);const y=Math.max(0,Math.min(Number(s.searchParams.get("ms"))||Te,Te));if(await new Promise(p=>{const a=M.get(n)||new Set;M.set(n,a);const r=u(()=>{clearTimeout(i),a.delete(r),!a.size&&M.get(n)===a&&M.delete(n),p()},"done"),i=setTimeout(r,y);a.add(r),t.on("close",r)}),t.writableEnded||t.destroyed)return;const m=await c();return m?m.answer?f(m):l(200,{state:"pending"}):l(404,{state:"gone"})}if(o==="answer"&&e.method==="POST"){const c=await k(e),f=await H(m=>{const p=T(m.prompts,n);if(!p)return{status:404,error:"That prompt has expired."};if(p.answer)return{status:409,error:"That prompt was already answered."};const{answer:a,error:r}=vt(p,c);if(r)return{status:400,error:r};p.answer=a;const i=de();return te(m,i),Ot(m,p,i),{rec:p,answer:a}});if(f.error)return l(f.status,{error:f.error});const{rec:g,answer:y}=f;h(`prompt ${n}: ${y.choice}`);for(const m of[...M.get(n)||[]])m();return B(g.route),l(200,{ok:!0})}if(o===null&&e.method==="DELETE"){const c=await H(f=>{const g=T(f.prompts,n);return delete f.prompts[n],g});return c&&(B(c.route),h(`prompt ${n}: its hook gave up`)),l(200,{ok:!0})}return!1}u(us,"promptRoute");const ls=u(({icon:e="",title:t,sub:n=""})=>`<div class="row">${e}<span class="body"><span class="text"><span class="title">${t}</span>${n?`<span class="sub">${n}</span>`:""}</span></span></div>`,"row");function it(e,t){const n=x.get(e)||{},o=n.exitCode!==void 0&&n.exitCode!==null?` (last exit: ${n.exitCode})`:"",s=C(t),[l,c]=s==="port"?["down",`down \xB7 nothing answers on port ${t.port}`]:s==="files"?["down","down \xB7 its folder is missing"]:n.child?["wait","started \xB7 waiting for it to answer"]:["down",`down \xB7 restarting${o}`],f=s==="cmd"?"The hub runs every site and restarts one that stops.":s==="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`${pe(`${t.title||e} is down`,'<meta http-equiv="refresh" content="5">')}
4
+ `),n.code=Ge(t)?Je(t):null;let i;try{i=De(a,l,{cwd:t.cwd||t.dir,env:h,stdio:["ignore",s,s],detached:!0})}catch(b){w.writeSync(s,`hub could not start it: ${b.message}
5
+ `),w.closeSync(s),Object.assign(n,{child:null,key:ae(t),state:"crashed",exitCode:null,nextStart:Date.now()+it(n.restarts++)}),O.set(e,n);return}w.closeSync(s),w.writeFileSync(re(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=u((b,k)=>{if(C.delete(i),n.child!==i)return;try{w.unlinkSync(re(e))}catch{}Date.now()-n.since>6e4&&(n.restarts=0),Object.assign(n,{child:null,state:n.stopping?"stopped":"crashed",exitCode:b??k,nextStart:Date.now()+it(n.restarts++),why:n.stopping?n.why:void 0}),n.stopping=!1,c(`${e} exited (${b??k})`)},"ended");i.on("error",b=>{c(`${e} spawn error: ${b.message}`),i.pid||S(b.code,null)}),i.on("exit",S)}u(Ea,"start");const it=u(e=>Math.min(6e4,1e3*2**Math.min(e,6)),"backoff");function se(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")},ot)}u(se,"stop");const oe=u((e,t)=>{try{process.kill(-e.pid,t)}catch{}},"signal");function Se(e){return new Promise(t=>{const n=Ee.connect({port:e,host:"127.0.0.1"}),s=u(a=>{n.destroy(),t(a)},"done");n.setTimeout(800,()=>s(!1)),n.on("connect",()=>s(!0)),n.on("error",()=>s(!1))})}u(Se,"probe");let ct=!0;const H=new Set;let ke=!1;const N=new Set,Da=3e4;async function La(e){const t=Date.now()+Da;for(;;){const n=j(g().sites,e),s=O.get(e);if(n&&s?.child&&s.up&&s.key===ae(n))return!0;if(Date.now()>=t)return!1;await new Promise(a=>setTimeout(a,250))}}u(La,"serverAnswers");async function ut(){if(ne)return;const{sites:e}=g();for(const t of[...O.keys()])H.has(t)||(j(e,t)?ye(j(e,t))||(c(`${t} no longer has a command; stopping it`),se(t),O.delete(t)):(c(`${t} left the manifest; stopping it`),se(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 Se(n.port));for(const[t,n]of Object.entries(e)){if(!ye(n))continue;ct&&Ra(t,n);const s=O.get(t);if(s&&s.child&&s.key!==ae(n)){c(`${t} entry changed; restarting it`),se(t),s.restarts=0,s.nextStart=Date.now()+1500;continue}const a=s?.child&&s.code&&!s.stopping&&!N.has(t)&&!H.has(t)?Je(n):null;if(a&&a!==s.code){if(!(await et(n.dir)).length){c(`${t} runs older code than its files; restarting it`),se(t),Object.assign(s,{restarts:0,nextStart:Date.now()+1500,why:"code"});continue}s.waitedOn!==a&&c(`${t} runs older code than its files; restarts once no builder runs`),s.waitedOn=a}(!s||!s.child&&Date.now()>=(s.nextStart||0))&&Ea(t,n)}ct=!1;for(const[t,n]of O){const s=j(e,t);s&&(n.up=n.child?await Se(s.port):!1,n.child&&n.up&&Object.assign(n,{state:"up"},n.stopping?{}:{why:void 0}))}}u(ut,"reconcile");const Ca=setInterval(()=>ut().catch(e=>c("reconcile failed:",e.message)),2e3);ut().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 lt=u(async()=>{try{for(const e of await $a())c(`linear: ${e}`)}catch(e){c("linear fallback failed:",e.message)}},"linearFallback");await lt();const Ha=6e4,dt=60*6e4,y={running:null,startedAt:0,firstAt:Date.now()+Ha,last:null,remaining:null,errorLoggedAt:0,linearErrorLoggedAt:0},A=Kt(),ht=u(()=>y.startedAt?sn({last:new Date(y.startedAt),...A.state(),remaining:y.remaining}):new Date(y.firstAt),"nextPollAt");function ft({changed:e,back:t}){e&&_.refresh(),t&&y.startedAt&&!y.running&&$e().catch(n=>c("poll failed:",n.message))}u(ft,"presenceChanged");function $e({flush:e=!0}={}){if(y.running)return y.running;const t=new Date;return y.startedAt=t.getTime(),y.running=(async()=>{const n=V();let s;try{s=await wn({every:Ne({present:!1,remaining:y.remaining}),manifest:g()})}catch(a){s={at:t.toISOString(),watched:0,failed:[a.message],changes:[],events:[],checked:[],merged:[],linear:{moved:[],failed:[]},rate:null}}y.remaining=s.rate?.remaining??null,s.merged.length&&(await qe({manifest:g(),log:c,routes:s.merged}),c(`merged on ${s.merged.join(", ")}: fetched and refreshing Main`),ge({manifest:g(),routes:s.merged}).catch(a=>c("merge refresh failed:",a.message))),await qe({manifest:g(),log:c});try{const a=await ba({manifest:g()});a.length&&ge({manifest:g(),routes:a}).catch(l=>c("pull refresh failed:",l.message))}catch(a){c("pull failed:",a.message)}try{s.failed.push(...(await yn({manifest:g()})).failed)}catch(a){s.failed.push(`incoming PRs: ${a.message}`)}s.failed.length&&Date.now()-y.errorLoggedAt>dt&&(y.errorLoggedAt=Date.now(),c(`github check: ${s.failed.length} step${s.failed.length===1?"":"s"} failed, first: ${s.failed[0]} (logged at most hourly)`)),s.changes.length&&c(`github check: ${s.changes.map(a=>`#${a.pr} ${a.changes.join(",")}`).join("; ")} \u2192 ${s.events.map(a=>a.route).join(", ")||"no board"}`);for(const{id:a,state:l}of s.linear.moved)c(`linear: ${a} \u2192 ${l}`);s.linear.failed.length&&Date.now()-y.linearErrorLoggedAt>dt&&(y.linearErrorLoggedAt=Date.now(),c(`linear check: ${s.linear.failed.length} step${s.linear.failed.length===1?"":"s"} failed, first: ${s.linear.failed[0]} (logged at most hourly)`));try{s.deaf=await kn({manifest:g()})}catch(a){s.deaf={pushed:[],failed:[a.message]}}(s.deaf.pushed.length||s.deaf.failed.length)&&c(`not listening: pushed ${s.deaf.pushed.join(", ")||"none"}${s.deaf.failed.length?`; ${s.deaf.failed.join("; ")}`:""}`);try{xn(g())}catch(a){c("could not record link faults:",a.message)}if(s.flushed=null,s.held=nn(t,n)!==null,e&&!s.held)try{const a=await He();a&&(s.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 y.last=s,s})().finally(()=>{y.running=null}),y.running}u($e,"runPoll");let pt=null;function Aa(){pt||=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(()=>{pt=null})}u(Aa,"runWakes");const _e=new Set,D={running:null};function mt(){if(D.running||H.size||N.size)return;const e=new Set(_e);_e.clear(),D.running=In({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(()=>Mn({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})}u(mt,"runSessionCheck"),await ga(g());const Ia=Date.now(),U=new Map;function G(e){e?(ie.delete(e),_.changed(e)):_.refresh()}u(G,"promptChanged");async function gt(){const{gone:e,rulesChanged:t}=await M(n=>{const s=Zt(n,{now:Date.now(),aliveSince:Ia}),a=he(n,xe());for(const l of s)delete n.prompts[l.id];return{gone:s,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`)}u(gt,"prunePrompts");const xe=u(()=>({running:Ke(),boards:R(g())}),"promptContext");async function wt(e){const t=R(g()).find(n=>n.owner?.session&&n.owner.session===e.session_id);if(!t)return null;try{const n=await _a(Ze(t.route).repo,t.dir,e);return n&&c(`permissions: /${t.route} was still ${e.hook_event_name==="PermissionDenied"?"refused":"asked about"} ${e.tool_input.command}`),n}catch(n){return c(`permissions: could not note /${t.route}'s refusal: ${n.message}`),null}}u(wt,"noteRefusal");let z=null,bt=null;async function Te(){return z?bt??=z.catch(()=>{}).then(()=>(bt=null,Te())):(z=(async()=>ca({gate:Ye(),resume:u(async e=>{const t=await We(R(g()),"usage-resumed",{window:e});for(const n of t.failed)c(n)},"resume")}))().finally(()=>{z=null}),z)}u(Te,"checkUsageGate"),setInterval(()=>{ge({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)),A.check().then(ft,e=>c("presence check failed:",e.message)).finally(()=>{!y.running&&Date.now()>=ht().getTime()&&$e().catch(e=>c("poll failed:",e.message))}),Aa(),ee.apply(),gt().catch(e=>c("prompt expiry failed:",e.message)).finally(mt)},15e3);function yt(){if(ne)return;ne=!0,clearInterval(Ca),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;ma().finally(()=>{e=!0});const t=Date.now()+ot;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)}u(yt,"shutdown"),process.on("SIGTERM",yt),process.on("SIGINT",yt);const St=new Map;function kt(e,t={n:3e3}){let n=0,s;try{s=w.readdirSync(e,{withFileTypes:!0})}catch{return 0}for(const a of s){if(t.n--<=0)break;if(a.name.startsWith(".")||a.name==="node_modules")continue;const l=$.join(e,a.name);if(a.isDirectory())n=Math.max(n,kt(l,t));else try{n=Math.max(n,w.statSync(l).mtimeMs)}catch{}}return n}u(kt,"newestMtime");function Ma(e,t){const n=St.get(e);if(n&&Date.now()-n.checked<3e4)return n.at;const s=kt(t.dir);return St.set(e,{at:s,checked:Date.now()}),s}u(Ma,"changedAt");const $t=u(e=>{try{return typeof e=="string"&&w.statSync(e).isDirectory()}catch{return!1}},"isFolder");function ve(){const{sites:e}=g();return Object.entries(e).sort(([t],[n])=>t.localeCompare(n)).map(([t,n])=>{const s=O.get(t)||{},a=B(n),l=a==="files"?$t(n.dir)?"up":"down":a==="port"?F.has(t)?F.get(t)?"up":"down":"starting":s.child?s.up?"up":"starting":s.state==="crashed"?"down":s.state||"starting";return{route:t,title:n.title||t,description:n.description||"",icon:n.icon||null,color:n.color||null,port:n.port??null,serves:a,state:l,exitCode:s.exitCode??null,owner:n.owner||null,restart:ea(n),changedAt:Ma(t,n)||null,created:n.created||null,log:$.join(q,t+".log")}})}u(ve,"status");const I=u(e=>String(e??"").replace(/[&<>"']/g,t=>({"&":"&amp;","<":"&lt;",">":"&gt;",'"':"&quot;","'":"&#39;"})[t]),"esc"),ie=new Map,_t=2e4,Ba=1500;async function Fa(e){try{const t=await fetch(`http://127.0.0.1:${e}/_claude/needs`,{signal:AbortSignal.timeout(Ba)});return t.ok?Ln(await t.json()):null}catch{return null}}u(Fa,"fetchNeeds");async function xt(e,{fresh:t=!1}={}){const n=e.filter(a=>a.state==="up"&&a.owner&&a.port);for(const a of n){const l=ie.get(a.route);(t===!0||t?.has?.(a.route)||!l||Date.now()-l.at>=_t)&&ie.set(a.route,{at:Date.now(),needs:Fa(a.port)})}const s=await Promise.all(n.map(a=>ie.get(a.route).needs));return new Map(n.map((a,l)=>[a.route,s[l]]))}u(xt,"waitingCounts");const Tt=u(e=>[...e.values()].reduce((t,n)=>t+(n?.count||0),0)+Be(null).length,"badgeTotal"),Oe=new Map;function Na(e){const t=new Map,n=new Map,s={running:Ke(),wakes:jn(Sn,{})};for(const a of e)try{const l=ze(a,s);n.set(a.route,l);let h=Oe.get(a.route);(!h||h.listen!==l||Date.now()-h.at>=_t)&&(h={at:Date.now(),listen:l,links:Tn(a,{listen:l})},Oe.set(a.route,h)),t.set(a.route,h.links)}catch(l){c(`links of ${a.route} failed:`,l.message),t.set(a.route,[])}return{links:t,listens:n}}u(Na,"boardLinks");async function ce({fresh:e=!1}={}){const t=new Date,n=ve(),s=await xt(n,{fresh:e}),a=g(),l=R(a),{links:h,listens:i}=Na(l),S=On(),b=new Map(l.map(r=>[r.route,ua(r.owner?.session)]).filter(([,r])=>r)),k=Cn(n,s,new Set(l.map(r=>r.route)),i,b),m=V(),p=tn(t,m);return{data:{code:S.running??null,total:Tt(s),start:Hn(k),sites:k,hub:An(k,h,vn(S)),home:w.realpathSync(Le.homedir()),starting:Bn(a,t.getTime(),Vn),usage:Pa({codexShare:Ie(Z())}),pause:Ye(),upgrade:W.state(),notifications:p?p.why==="off"?{state:"off",text:"Off"}:{state:"held",text:cn(an(p,t))}:{state:"on",text:"On"},away:!A.state().present},links:h,rows:n,code:S}}u(ce,"sidebarState");function vt(e,t,n,s,a){let l=Buffer.isBuffer(a)?a:Buffer.from(a);const h={...s,vary:[s.vary,"accept-encoding"].filter(Boolean).join(", ")};l.length>=1024&&/\bgzip\b/.test(e.headers["accept-encoding"]||"")&&(l=Ct.gzipSync(l),h["content-encoding"]="gzip"),h["content-length"]=l.length,t.writeHead(n,h),t.end(e.method==="HEAD"?void 0:l)}u(vt,"sendText");async function Ua(e){const{data:t}=await ce(),n=Ce.createContext({});for(const s of["ui-phase-bars.js","ui-page-header.js","ui-sidebar.js"])Ce.runInContext(w.readFileSync($.join(me,s),"utf8"),n);return sa({sidebar:n.BoardUI.sidebar(t,{here:e}),sidebarData:JSON.stringify(t).replace(/</g,"\\u003c"),touchIcon:ue(180)})}u(Ua,"shellPage");const W=Sa({root:nt(),builders:u(async()=>(await Promise.all(R(g()).map(e=>et(e.dir)))).flat(),"builders"),run:Ga,log:c,changed:u(()=>_.refresh(),"changed")});function Ga(){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 s=De(process.execPath,[$.join(nt(),"bin","kickflip.mjs"),"update"],{detached:!0,stdio:["ignore",t,t]});w.closeSync(t),s.unref(),s.on("error",a=>n({ok:!1,error:a.message})),s.on("exit",a=>{const l=w.readFileSync(e,"utf8").trim().split(`
7
+ `).at(-1);c(`kickflip update exited ${a}: ${l}`),n(a===0?{ok:!0}:{ok:!1,error:l.replace(/^kickflip: /,"")})})})}u(Ga,"runUpdate"),W.check(),setInterval(()=>W.check(),ka);const _=oa({targets:u(()=>Object.entries(g().sites).filter(([,e])=>Ge(e)&&e.port).map(([e,t])=>({route:e,port:t.port})),"targets"),watched:u(()=>R(g()).map(({route:e,dir:t})=>({route:e,dir:t})),"watched"),sidebar:u(async({fresh:e})=>(await ce({fresh:e})).data,"sidebar"),log:c});function ue(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}`}u(ue,"iconUrl");const za=u(()=>JSON.stringify({name:"Claude sites",short_name:"Claude",start_url:"/",scope:"/",display:"standalone",background_color:"#121514",theme_color:"#121514",icons:[{src:ue(192),sizes:"192x192",type:"image/png"},{src:ue(512),sizes:"512x512",type:"image/png"}]}),"manifestJson");async function E(e,t=16*1024){let n="";for await(const s of e)if(n+=s,n.length>t)throw Object.assign(new Error("too large"),{status:413});return n}u(E,"readBody");async function x(e,t){if(!pe(e.headers))throw Object.assign(new Error("Send the body as application/json."),{status:403});const n=await E(e,t);return n?JSON.parse(n):{}}u(x,"readJSONBody");const Ot=u((e,t="")=>`<!doctype html><html lang="en" class="hub-view"><head><meta charset="utf-8"><meta name="viewport" content="width=device-width, initial-scale=1, viewport-fit=cover">
8
+ <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="${ue(180)}">
9
+ <meta name="apple-mobile-web-app-title" content="Claude">${t}<link rel="stylesheet" href="/_hub/colors.css?v=${Qe()}"><link rel="stylesheet" href="/_hub/hub.css"></head><body><main>`,"head"),Wa=u(e=>/\bapplication\/json\b/.test(e.headers.accept||""),"wantsJSON"),Ja=u(()=>({running:!!y.running,nextAt:ht(),last:y.last,present:A.state().present,every:Ne({present:A.state().present,remaining:y.remaining}),low:y.remaining!==null&&y.remaining<on}),"pollState");async function le(e){await gt();const t=new Date,{data:n,links:s,rows:a,code:l}=await ce();return{...un({now:t,st:Ae(t),poll:Ja(),sidebar:n,rows:a,links:s,rules:fe(),integrations:await ha.rows(),code:l,host:e.headers["x-forwarded-host"]||e.headers.host||"",codexShare:Ie(Z()),claudeModel:Gt(),codex:Z(),awake:Ue(t),remembered:new Map(R(g()).map(h=>[h.route,Fe(h.route)]))}),prompts:Be(null).map(Yt),remembered:Fe(null)}}u(le,"hubData");const J=u(async()=>{const e=new Date,t={until:Ue(e),unsupported:gn()};return{...ln({now:e,st:Ae(e),rules:fe(),route:await Jt(),linear:Q(),awake:t}),closed:Xn()}},"settingsData"),P=u((e,t)=>{e.writeHead(303,{location:t,"cache-control":"no-store"}),e.end()},"redirect");async function Ka(e,t,n){const s=u((r,o,d,f={})=>{t.writeHead(r,{"content-type":o,"cache-control":"no-store",...f}),t.end(d)},"send"),a=u((r,o)=>{const d=_n();return s(r,"application/json",JSON.stringify(o),d?{"x-hub-code":d}:{})},"sendJSON"),l=u(({route:r,started:o})=>(o.then(({error:d})=>{d&&c(`new board /${r} didn't start: ${d}`),_.refresh()}),_.refresh(),a(200,{route:r})),"starting"),h=Wa(e),i=n.pathname,S=/^\/_hub\/close-board\/([a-z0-9-]+)$/.exec(i);if(S&&e.method==="GET")try{return a(200,await Zn(S[1]))}catch(r){return a(r.status||400,{error:r.message})}if(["/_hub/close-board","/_hub/reopen-board"].includes(i)&&e.method==="POST"){let r;try{const o=await x(e);if(r=o.route,i==="/_hub/reopen-board"){const v=await Yn(r);return _.refresh(),a(200,v)}if(H.has(r))return a(409,{error:"That board is already closing."});H.add(r);const d=u(()=>H.delete(r),"release");t.once("finish",d),t.once("close",d),await D.running;const f=await Qn(r,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"){if(!await tt(e))return a(403,{error:"Only the signed-in owner can restart a board."});let r;try{if(r=(await x(e)).route,N.has(r))return a(409,{error:"That board is already restarting."});N.add(r),await D.running;const o=await qn(r,{serverUp:La,portUp:Se,onStarting:u(d=>c(`restart /${r}: ${d}\u2019s session is back, its server is still starting`),"onStarting")});return c(`restarted /${r}: server answers, session ${o.session} resumed`),a(200,o)}catch(o){return r&&c(`restart /${r} failed: ${o.message}`),a(o.status||400,{error:o.message})}finally{N.delete(r),_.refresh()}}if(i==="/_hub/restart-hub"&&e.method==="POST")try{return await x(e),await tt(e)?ke?a(409,{error:"The hub is already restarting."}):(ke=!0,t.once("finish",()=>{c("restarting the hub from the hub page");const r=xa("hub");r&&(ke=!1,c(`hub restart failed: ${r}`))}),a(200,{restarting:!0,command:Ta().restartCommand})):a(403,{error:"Only the signed-in owner can restart the hub."})}catch(r){return a(r.status||400,{error:r.message})}if(i==="/_hub/upgrade"&&e.method==="POST")try{return await x(e),a(200,{upgrade:await W.request()})}catch(r){return a(r.status||400,{error:r.message})}if(i==="/_hub/presence"&&e.method==="POST")return await x(e),ft(A.beat()),t.writeHead(204,{"cache-control":"no-store"}),t.end();if(i==="/_hub/wake"&&e.method==="POST")try{const{route:r}=await x(e),o=R(g()).find(d=>d.route===r);return o?(_e.add(r),await D.running,mt(),await D.running,Oe.clear(),_.changed(r),a(200,{route:r,listen:ze(o)})):a(404,{error:"That board is not open."})}catch(r){return a(r.status||400,{error:r.message})}if(i==="/_hub/repos"&&e.method==="GET")try{return a(200,await L.list(g(),{fresh:n.searchParams.get("fresh")==="1"}))}catch(r){return a(r.status||502,{error:r.message})}if(i==="/_hub/branches"&&e.method==="GET")try{return a(200,await L.branches(n.searchParams.get("github")??n.searchParams.get("remote")))}catch(r){return a(r.status||502,{error:r.message})}if(i==="/_hub/repo"&&e.method==="GET")return Nn(n,{repos:L,manifest:g(),sendJSON:a});const b=/^\/_hub\/board-repo\/([a-z0-9-]+)$/.exec(i);if(b&&["GET","POST"].includes(e.method))try{const r=e.method==="GET"?Ze(b[1]):await Un(b[1],await x(e),{repos:L});return e.method==="POST"&&_.refresh(),a(200,r)}catch(r){return a(r.status||400,{error:r.message})}if(i==="/_hub/new-board"&&e.method==="POST"){if(!pe(e.headers))return s(403,"text/plain; charset=utf-8","Send name, github, branch and purpose as application/json.");let r;try{r=await x(e)}catch{return s(400,"text/plain; charset=utf-8","Send name, github, branch and purpose as JSON.")}try{return l(await Wn(r,{repos:L,changed:u(()=>_.refresh(),"changed")}))}catch(o){if(o.status)return s(o.status,"text/plain; charset=utf-8",o.message);throw o}}const k=/^\/_hub\/new-board\/([a-z0-9-]+)\/retry$/.exec(i);if(k&&e.method==="POST"){if(!pe(e.headers))return s(403,"text/plain; charset=utf-8","Send the retry as application/json.");try{return l(await Jn(k[1],{repos:L,changed:u(()=>_.refresh(),"changed")}))}catch(r){if(r.status)return s(r.status,"text/plain; charset=utf-8",r.message);throw r}}const m=/^\/_hub\/new-board\/([a-z0-9-]+)$/.exec(i);if(m&&e.method==="DELETE")try{const r=await Kn(m[1]);return _.refresh(),a(200,r)}catch(r){if(r.status)return s(r.status,"text/plain; charset=utf-8",r.message);throw r}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 r=["127.0.0.1","::1","::ffff:127.0.0.1"],o=(e.headers.host||"").replace(/:\d+$/,"");if(!r.includes(e.socket.remoteAddress)||!["127.0.0.1","localhost","[::1]"].includes(o)||e.headers.origin||Object.keys(e.headers).some(f=>f==="forwarded"||f.startsWith("x-forwarded-")))return a(403,{error:`${i==="/_hub/pull"?"Pulls":"Builds"} can only be started from the host over loopback.`});let d;try{d=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 wa(d,{manifest:g()})):a(202,await pa(d,{manifest:g(),log:c}))}catch(f){if(f instanceof fa)return a(f.status,{error:f.message});throw f}}if(i==="/sw.js")return s(200,"text/javascript; charset=utf-8",w.readFileSync($.join(X,"sw.js")),{"service-worker-allowed":"/"});if(i==="/manifest.webmanifest")return s(200,"application/manifest+json",za());const p=/^\/_hub\/icon-(180|192|512)\.png$/.exec(i);if(p)try{return s(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 r=n.searchParams.get("v")===Qe()?"public, max-age=31536000, immutable":"no-cache";try{return s(200,"text/css; charset=utf-8",w.readFileSync(ja),{"cache-control":r})}catch{t.writeHead(404).end();return}}if(i==="/_hub/kickflip.png")try{return s(200,"image/png",w.readFileSync($.join(me,"kickflip.png")),{"cache-control":"max-age=86400"})}catch{t.writeHead(404).end();return}if(i==="/_hub/hub.css")return s(200,"text/css; charset=utf-8",w.readFileSync($.join(me,"hub.css")));if(i==="/_hub/sidebar.json")return a(200,(await ce({fresh:n.searchParams.get("fresh")==="1"})).data);if(i==="/_hub/stream"&&e.method==="GET"){_.handle(e,t);return}if(i.startsWith("/_hub/app/")){const r=na(i.slice(10));if(!r){t.writeHead(404).end();return}const o=$.extname(r),d={".js":"text/javascript; charset=utf-8",".css":"text/css; charset=utf-8",".png":"image/png"}[o],f=n.searchParams.get("v")===ra(r)?"public, max-age=31536000, immutable":"no-cache";return o===".png"?s(200,d,w.readFileSync(r),{"cache-control":f}):vt(e,t,200,{"content-type":d,"cache-control":f},w.readFileSync(r))}if(i==="/_hub/api/hub")return a(200,await le(e));if(i==="/_hub/api/settings")return a(200,await J());if(i==="/_hub/push/key")return a(200,{key:Ht()});if(i==="/_hub/push/subscribe"&&e.method==="POST"){const r=e.headers["x-forwarded-host"]||e.headers.host;return a(200,{count:At(await x(e),`https://${r}/`,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 r=await Mt({title:"Claude",body:"Test notification from the hub.",url:"/",tag:"hub-test"});return a(200,{sent:r.filter(o=>o.status>=200&&o.status<300).length,results:r})}if(i==="/_hub/settings/notifications"&&e.method==="POST"){const r=new URLSearchParams(await E(e)),o=r.get("set"),d=o==="on"?{on:r.get("value")==="on"}:o==="weekends"?{weekends_off:r.get("value")==="on"}:r.has("snooze")?{snooze:r.get("snooze")}:null,f="Pass set=on or set=weekends with value=on, or snooze.";if(!d)return h?a(400,{error:f}):s(400,"text/plain; charset=utf-8",f);let v;try{v=await Nt(d)}catch(K){if(K.status!==400)throw K;return h?a(400,{error:hn}):P(t,"/_hub/?refused=snooze#notifications")}return v.dropped&&c(`notifications off: dropped ${v.dropped} queued push${v.dropped===1?"":"es"}`),h?a(200,await J()):P(t,"/_hub/#notifications")}if(i==="/_hub/settings/keep-awake"&&e.method==="POST"){const r=new URLSearchParams(await E(e));try{mn(r.get("keep_awake"))}catch(o){if(o.status===409)return h?a(409,{error:o.message}):s(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 r=new URLSearchParams(await E(e));try{Ut(r.get("value"),Z({fresh:!0}))}catch(o){if(o.status!==400&&o.status!==409)throw o;return h?a(o.status,{error:o.message}):s(o.status,"text/plain; charset=utf-8",o.message)}return h?a(200,await le(e)):P(t,"/_hub/")}if(i==="/_hub/settings/claude-model"&&e.method==="POST"){const r=new URLSearchParams(await E(e));try{zt(r.get("value"))}catch(o){if(o.status!==400)throw o;return h?a(400,{error:o.message}):s(400,"text/plain; charset=utf-8",o.message)}return h?a(200,await le(e)):P(t,"/_hub/")}if(["/_hub/settings/pause-at","/_hub/settings/keep-going"].includes(i)&&e.method==="POST"){const r=new URLSearchParams(await E(e));try{i.endsWith("/pause-at")?la(r.get("value")):da(r.get("value")),await Te()}catch(o){if(o.status!==400&&o.status!==409)throw o;return h?a(o.status,{error:o.message}):s(o.status,"text/plain; charset=utf-8",o.message)}return h?a(200,await le(e)):P(t,"/_hub/")}if(i==="/_hub/settings/linear"&&e.method==="POST"){const r=new URLSearchParams(await E(e,4096));try{r.has("remove")?(va(),c("linear: key removed"),await lt()):c(`linear: key ${(await Oa(r.get("key"))).refused?"refused by Linear":"saved"}`)}catch(o){if(!(o instanceof we))throw o;return h?a(o.status,{error:o.message}):s(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 r=Q();if(!r.set||r.refused)return a(200,r);const o=at();try{const d=await o.teams();return a(200,{...Q(),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 we))throw d;return a(200,{...Q(),error:d.message})}}if(i==="/_hub/linear/projects"&&e.method==="POST"){const r=at();if(!r)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 d=typeof o.name=="string"?o.name.trim():"";if(!d||d.length>80||typeof o.team!="string"||!/^[A-Z][A-Z0-9]*$/.test(o.team))return a(400,{error:"Name the project and its team."});try{return a(200,await r.createProject(o.team,d))}catch(f){if(!(f instanceof we))throw f;return a(f.status,{error:f.message})}}if(i==="/_hub/settings/quiet"&&e.method==="POST"){const r=new URLSearchParams(await E(e)),o=Object.fromEntries(["start","end"].filter(d=>r.has(d)).map(d=>[d,r.get(d).trim()]));r.get("set")==="hold"&&(o.hold=r.get("value")||"off");try{Bt(rn(V(),o))}catch(d){if(d.status!==400)throw d;return h?a(400,{error:d.message}):s(400,"text/plain; charset=utf-8",d.message)}return _.refresh(),h?a(200,await J()):P(t,"/_hub/#quiet")}if(i==="/_hub/settings/rules"&&e.method==="POST"){let r,o;try{r=await x(e,64*1024),o=Rn(fe(),r.rules,r.base??[]),Ve(o.changes)&&En(o.rules)}catch(f){if(!(f instanceof Pn)&&!(f instanceof SyntaxError))throw f;return a(400,{error:f.message})}if(!Ve(o.changes))return a(200,{rules:o.rules,changed:!1,events:[],failed:[]});const d=await We(R(g()),"rules",{scope:"hub",...o.changes});return c(`hub rules saved: rules \u2192 ${d.events.map(f=>f.route).join(", ")||"no board"}${d.failed.length?`; ${d.failed.join("; ")}`:""}`),a(200,{rules:o.rules,changed:!0,...d})}if(i==="/_hub/push/flush"&&e.method==="POST"){const r=await He();return r&&c(`push queue: ${r.ok?"sent":"could not send"} ${r.count} queued (from the settings page)`),h?a(200,dn(r)):P(t,"/_hub/")}if(i==="/_hub/poll"&&e.method==="POST"){const r=await x(e);return a(200,await $e({flush:r.flush!==!1}))}if(i==="/_hub/prompts"&&e.method==="POST"){let r;try{r=await x(e,8*1024*1024)}catch(f){if(f.status)throw f;return a(400,{error:"Send the hook input as JSON."})}if(!r?.tool_name||r.hook_event_name&&r.hook_event_name!=="PermissionRequest")return a(400,{error:"Send a PermissionRequest hook input."});const{answer:o,rec:d}=await M(f=>{const v=xe(),K=v.boards.find(Pe=>Pe.owner?.session&&Pe.owner.session===r.session_id)||null;he(f,v)&&_.refresh();const je=en(f,r);if(je)return{answer:je};const de=Qt(r,{board:K,live:v.running?.get(r.session_id)});return f.prompts[de.id]=de,{rec:de}});return o?a(200,{state:"answered",answer:o}):(G(d.route),await wt(r),c(`prompt ${d.id}: ${d.route?`/${d.route}`:d.cwd} ${d.tool}${d.action?` (${d.action})`:""}`),Wt(Xt(d),{kind:"prompt",immediate:!0}).catch(f=>c(`prompt ${d.id}: push failed: ${f.message}`)),a(200,{id:d.id,deadline:d.deadline}))}if(i==="/_hub/permission-denied"&&e.method==="POST"){let r;try{r=await x(e,8*1024*1024)}catch(o){if(o.status)throw o;return a(400,{error:"Send the hook input as JSON."})}return r?.hook_event_name!=="PermissionDenied"?a(400,{error:"Send a PermissionDenied hook input."}):a(200,{noted:await wt(r)})}const T=/^\/_hub\/prompts\/([\w-]+)(\/wait|\/answer)?$/.exec(i);return T?Va(e,t,{id:T[1],op:T[2]?.slice(1)||null,url:n,sendJSON:a}):!1}u(Ka,"hubRoute");async function Va(e,t,{id:n,op:s,url:a,sendJSON:l}){if(s==="wait"&&e.method==="GET"){const h=u(()=>M(m=>{const p=j(m.prompts,n);return p?.answer?delete m.prompts[n]:p&&(p.seen=new Date().toISOString()),p}),"seen"),i=u(m=>(G(m.route),l(200,{state:"answered",answer:m.answer})),"take"),S=await h();if(!S)return l(404,{state:"gone"});if(S.answer)return i(S);const b=Math.max(0,Math.min(Number(a.searchParams.get("ms"))||Me,Me));if(await new Promise(m=>{const p=U.get(n)||new Set;U.set(n,p);const T=u(()=>{clearTimeout(r),p.delete(T),!p.size&&U.get(n)===p&&U.delete(n),m()},"done"),r=setTimeout(T,b);p.add(T),t.on("close",T)}),t.writableEnded||t.destroyed)return;const k=await h();return k?k.answer?i(k):l(200,{state:"pending"}):l(404,{state:"gone"})}if(s==="answer"&&e.method==="POST"){const h=await x(e),i=await M(k=>{const m=j(k.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:T}=Vt(m,h);if(T)return{status:400,error:T};m.answer=p;const r=xe();return he(k,r),qt(k,m,r),{rec:m,answer:p}});if(i.error)return l(i.status,{error:i.error});const{rec:S,answer:b}=i;c(`prompt ${n}: ${b.choice}`);for(const k of[...U.get(n)||[]])k();return G(S.route),l(200,{ok:!0})}if(s===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`)),l(200,{ok:!0})}return!1}u(Va,"promptRoute");const Za=u(({icon:e="",title:t,sub:n=""})=>`<div class="row">${e}<span class="body"><span class="text"><span class="title">${t}</span>${n?`<span class="sub">${n}</span>`:""}</span></span></div>`,"row");function jt(e,t){const n=O.get(e)||{},s=n.exitCode!==void 0&&n.exitCode!==null?` (last exit: ${n.exitCode})`:"",a=B(t),[l,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${s}`],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`${Ot(`${t.title||e} is down`,'<meta http-equiv="refresh" content="5">')}
8
10
  <a class="back" href="/_hub/">\u2039 Hub</a>
9
- <h1>${j(t.title||e)} isn't answering</h1>
10
- <p class="lede">${j(f)}</p>
11
+ <h1>${I(t.title||e)} isn't answering</h1>
12
+ <p class="lede">${I(i)}</p>
11
13
  <h2 class="label">Site</h2>
12
- <div class="group">${ls({icon:on(e,t),title:j(t.title||e),sub:`<span class="st ${l}">${j(c)}</span>`})}</div>
14
+ <div class="group">${Za({icon:Dn(e,t),title:I(t.title||e),sub:`<span class="st ${l}">${I(h)}</span>`})}</div>
13
15
  <p class="note">This page retries every 5 seconds.</p>
14
- </main></body></html>`}u(it,"downPage");const we=u(e=>new URL(e.url.replace(/^\/+/,"/"),"http://hub"),"requestURL"),ct=Se.createServer((e,t)=>{const n=we(e);if(e.method!=="GET"&&e.method!=="HEAD"&&_n(e.headers)){t.writeHead(403,{"content-type":"text/plain; charset=utf-8","cache-control":"no-store"}),t.end("Requests must come from the app\u2019s own pages.");return}const o=/^\/([^/]+)\/try\/([^/]+)(\/|$)/.exec(n.pathname);let s=null;if(o&&O(w()).some(r=>r.route===o[1])){if(!o[3]){t.writeHead(302,{location:n.pathname+"/"+n.search}).end();return}if(s=Ie(o[1],o[2],{manifest:w(),visit:!0}),!s){t.writeHead(404,{"content-type":"text/html; charset=utf-8","cache-control":"no-store"}),t.end(`${pe("Not being served")}<a class="back" href="/${j(o[1])}/">\u2039 Board</a><h1>${j(o[2].toUpperCase())} isn't being served. Tap Serve on its card.</h1></main></body></html>`);return}}const l=new Set(Object.keys(w().sites));if(!s&&Le({method:e.method,pathname:n.pathname,search:n.search,dest:Tn(e.headers),sites:l})==="shell"){const r=n.pathname==="/"?"":n.pathname.startsWith("/_hub/")?n.pathname.slice(1):decodeURIComponent(n.pathname.split("/")[1]);os(r).then(i=>at(e,t,200,{"content-type":"text/html; charset=utf-8","cache-control":"no-cache",vary:"sec-fetch-dest","x-app-shell":"1"},i)).catch(i=>{t.headersSent||t.writeHead(500,{"content-type":"text/plain"}),t.end(String(i.message||i))});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:ie,pid:process.pid},sites:fe()},null,2));return}if(n.pathname==="/_hub/needs.json"){const r=fe();nt(r,{fresh:n.searchParams.get("fresh")==="1"}).then(i=>{t.writeHead(200,{"content-type":"application/json","cache-control":"no-store"}),t.end(JSON.stringify({total:st(i),sites:Object.fromEntries(r.map(d=>[d.route,i.get(d.route)??null]))}))}).catch(i=>{t.headersSent||t.writeHead(500,{"content-type":"text/plain"}),t.end(String(i.message||i))});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/")){cs(e,t,n).then(r=>{r===!1&&t.writeHead(404).end()}).catch(r=>{t.headersSent||t.writeHead(r.status||500,{"content-type":"text/plain"}),t.end(String(r.message||r))});return}const c=n.pathname.split("/")[1],f=T(w().sites,c);if(!f){t.writeHead(404,{"content-type":"text/html; charset=utf-8"}),t.end(`${pe("No Site")}<a class="back" href="/_hub/">\u2039 Hub</a><h1>No site at /${j(c)}</h1></main></body></html>`);return}if(n.pathname==="/"+c){t.writeHead(302,{location:"/"+c+"/"+n.search}).end();return}const g=s?`/${c}/try/${o[2]}`:"/"+c,y=e.url.slice(g.length)||"/",m=Le({pathname:n.pathname,dest:"document",sites:l})==="shell";if(!s&&C(f)==="files"){if(!et(f.dir)){t.writeHead(502,{"content-type":"text/html; charset=utf-8","cache-control":"no-store"}),t.end(it(c,f));return}Un(e,t,{dir:f.dir,rest:y,prefix:g,headers:m?{vary:"sec-fetch-dest"}:{}}).catch(r=>{t.headersSent||t.writeHead(500,{"content-type":"text/plain"}),t.end(String(r.message||r))});return}const p={...e.headers,"x-forwarded-prefix":g,"x-forwarded-host":e.headers["x-forwarded-host"]||e.headers.host,"x-forwarded-proto":e.headers["x-forwarded-proto"]||"https"},a=Se.request({host:"127.0.0.1",port:s?.port||f.port,method:e.method,path:y,headers:p},r=>{const i={...r.headers};m&&(i.vary=[i.vary,"sec-fetch-dest"].filter(Boolean).join(", ")),typeof i.location=="string"&&i.location.startsWith("/")&&!i.location.startsWith("//")&&!i.location.startsWith(g+"/")&&i.location!==g&&(i.location=g+i.location),t.writeHead(r.statusCode,i),r.pipe(t),r.on("close",()=>{r.complete||t.destroy()})});t.on("close",()=>{t.writableFinished||a.destroy()}),a.on("error",()=>{if(t.headersSent){t.destroy();return}t.writeHead(502,{"content-type":"text/html; charset=utf-8","cache-control":"no-store"}),t.end(it(c,f))}),e.pipe(a)});ct.on("upgrade",(e,t,n)=>{const o=we(e).pathname.split("/")[1],s=T(w().sites,o),l=/^\/[^/]+\/try\/([^/]+)\//.exec(we(e).pathname),c=l&&O(w()).some(p=>p.route===o),f=c?Ie(o,l[1],{manifest:w(),visit:!0}):s;if(!f?.port){t.destroy();return}const g=c?`/${o}/try/${l[1]}`:`/${o}`,y=e.url.slice(g.length)||"/",m=$e.connect(f.port,"127.0.0.1",()=>{const p=[`${e.method} ${y} HTTP/${e.httpVersion}`];for(let a=0;a<e.rawHeaders.length;a+=2)e.rawHeaders[a].toLowerCase()!=="x-forwarded-prefix"&&p.push(`${e.rawHeaders[a]}: ${e.rawHeaders[a+1]}`);p.push(`X-Forwarded-Prefix: ${g}`),m.write(p.join(`\r
16
+ </main></body></html>`}u(jt,"downPage");const Pt=u(e=>new URL(e.url.replace(/^\/+/,"/"),"http://hub"),"requestURL"),Rt=Re.createServer((e,t)=>{const n=Pt(e);if(e.method!=="GET"&&e.method!=="HEAD"&&ta(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 s=new Set(Object.keys(g().sites));if(Xe({method:e.method,pathname:n.pathname,search:n.search,dest:aa(e.headers),sites:s})==="shell"){const m=n.pathname==="/"?"":n.pathname.startsWith("/_hub/")?n.pathname.slice(1):decodeURIComponent(n.pathname.split("/")[1]);Ua(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:be,pid:process.pid},sites:ve()},null,2));return}if(n.pathname==="/_hub/needs.json"){const m=ve();xt(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:Tt(p),sites:Object.fromEntries(m.map(T=>[T.route,p.get(T.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/")){Ka(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(g().sites,a);if(!l){t.writeHead(404,{"content-type":"text/html; charset=utf-8"}),t.end(`${Ot("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=Xe({pathname:n.pathname,dest:"document",sites:s})==="shell";if(B(l)==="files"){if(!$t(l.dir)){t.writeHead(502,{"content-type":"text/html; charset=utf-8","cache-control":"no-store"}),t.end(jt(a,l));return}ya(e,t,{dir:l.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 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"},k=Re.request({host:"127.0.0.1",port:l.port,method:e.method,path:i,headers:b},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||k.destroy()}),k.on("error",()=>{if(t.headersSent){t.destroy();return}t.writeHead(502,{"content-type":"text/html; charset=utf-8","cache-control":"no-store"}),t.end(jt(a,l))}),e.pipe(k)});Rt.on("upgrade",(e,t,n)=>{const s=Pt(e).pathname.split("/")[1],a=j(g().sites,s);if(!a?.port){t.destroy();return}const l=`/${s}`,h=e.url.slice(l.length)||"/",i=Ee.connect(a.port,"127.0.0.1",()=>{const S=[`${e.method} ${h} HTTP/${e.httpVersion}`];for(let b=0;b<e.rawHeaders.length;b+=2)e.rawHeaders[b].toLowerCase()!=="x-forwarded-prefix"&&S.push(`${e.rawHeaders[b]}: ${e.rawHeaders[b+1]}`);S.push(`X-Forwarded-Prefix: ${l}`),i.write(S.join(`\r
15
17
  `)+`\r
16
18
  \r
17
- `),n&&n.length&&m.write(n),m.pipe(t),t.pipe(m)});m.on("error",()=>t.destroy()),t.on("error",()=>m.destroy()),t.on("close",()=>m.destroy()),m.on("close",()=>t.destroy())}),ct.listen(ie,"127.0.0.1",()=>h(`hub on http://127.0.0.1:${ie}/ (manifest ${Be})`));
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())}),Rt.listen(be,"127.0.0.1",()=>c(`hub on http://127.0.0.1:${be}/ (manifest ${rt})`));
@@ -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 v=Object.defineProperty;var o=(s,u)=>v(s,"name",{value:u,configurable:!0});import y from"node:fs";import h from"node:path";import L from"node:os";class d extends Error{static{o(this,"LinearError")}constructor(u,r=502){super(u),this.status=r}}const f=o(()=>h.join(L.homedir(),".claude","kickflip","linear.json"),"keyFile"),S=o(()=>process.env.KICKFLIP_LINEAR_URL||"https://api.linear.app/graphql","endpoint");function k(){try{const s=JSON.parse(y.readFileSync(f(),"utf8"));return typeof s?.key=="string"&&s.key?s:null}catch{return null}}o(k,"readLinearKey");function g(){const s=k();return s?{set:!0,workspace:s.workspace,url_key:s.url_key}:{set:!1}}o(g,"linearStatus");function $(s,{fetch:u=globalThis.fetch}={}){async function r(t,e={}){let n;try{n=await u(S(),{method:"POST",headers:{"content-type":"application/json",authorization:s},body:JSON.stringify({query:t,variables:e}),signal:AbortSignal.timeout(15e3)})}catch(c){throw new d(`Could not reach Linear: ${c.message}`)}const a=await n.json().catch(()=>null);if(n.status===401||a?.errors?.some(c=>c.extensions?.type==="authentication error"))throw new d("Linear refused the key.",400);if(a?.errors?.length){const c=a.errors[0];throw Object.assign(new d(`Linear: ${c.message}`),{notFound:c.extensions?.type==="not found"||/not found/i.test(c.message)})}if(!n.ok||!a?.data)throw new d(`Linear answered ${n.status}.`);return a.data}o(r,"call");const m="id key name states { nodes { id name type position } } projects(first: 100) { nodes { id name } }";async function p(t){return(await r(`query($key: String!) { teams(filter: { key: { eq: $key } }) { nodes { ${m} } } }`,{key:t})).teams.nodes[0]||(()=>{throw new d(`Linear has no team ${t}.`,400)})()}o(p,"team");const i=o((t,e)=>t.projects.nodes.find(n=>n.id===e)||t.projects.nodes.find(n=>n.name.toLowerCase()===String(e).toLowerCase())||(()=>{throw new d(`Linear's ${t.key} team has no project ${e}.`,400)})(),"projectOf"),l={async viewer(){const t=await r("query { viewer { id name } organization { name urlKey } }");return{viewer:{id:t.viewer.id,name:t.viewer.name},workspace:t.organization.name,url_key:t.organization.urlKey}},async teams(){return(await r("query { teams(first: 100) { nodes { id key name projects(first: 100) { nodes { id name } } } } }")).teams.nodes.map(e=>({id:e.id,key:e.key,name:e.name,projects:e.projects.nodes.map(n=>({id:n.id,name:n.name}))}))},async createProject(t,e){const n=await p(t),a=await r("mutation($input: ProjectCreateInput!) { projectCreate(input: $input) { success project { id name } } }",{input:{name:e,teamIds:[n.id]}});if(!a.projectCreate.success)throw new d("Linear did not create the project.");return{...a.projectCreate.project,team:n.key}},async createIssue({team:t,project:e,title:n,description:a}){const c=await p(t),j={teamId:c.id,projectId:i(c,e).id,title:n,...a?{description:a}:{}},w=await r("mutation($input: IssueCreateInput!) { issueCreate(input: $input) { success issue { identifier url } } }",{input:j});if(!w.issueCreate.success)throw new d("Linear did not create the issue.");return w.issueCreate.issue},async issue(t){try{const e=await r("query($id: String!) { issue(id: $id) { identifier team { key } assignee { id } state { id name type } } }",{id:t});return e.issue?{identifier:e.issue.identifier,team:e.issue.team.key,assignee:e.issue.assignee?.id??null,state:e.issue.state}:null}catch(e){if(e.notFound)return null;throw e}},async moveIssue(t,e){const n=await l.issue(t);if(!n)throw new d(`Linear has no issue ${t}.`,400);if(["completed","canceled"].includes(n.state.type))return null;const a=I((await p(n.team)).states.nodes,e);if(!a||a.id===n.state.id)return null;if(!(await r("mutation($id: String!, $stateId: String!) { issueUpdate(id: $id, input: { stateId: $stateId }) { success } }",{id:t,stateId:a.id})).issueUpdate.success)throw new d("Linear did not move the issue.");return a.name}};return l}o($,"linearClient");function I(s,u){const r=[...s].sort((i,l)=>i.position-l.position),m=o(i=>r.filter(l=>l.type===i),"of"),p=o(i=>/review/i.test(i.name),"review");return u==="started"?m("started").find(i=>/^in progress$/i.test(i.name))||m("started").find(i=>!p(i))||null:u==="review"?m("started").find(p)||null:u==="done"&&(m("completed").find(i=>/^done$/i.test(i.name))||m("completed")[0])||null}o(I,"stateFor");async function O(s,{fetch:u}={}){const r=typeof s=="string"?s.trim():"";if(!r||/\s/.test(r)||r.length>200)throw new d("Paste a Linear personal API key.",400);const m=await $(r,{fetch:u}).viewer(),p=f();y.mkdirSync(h.dirname(p),{recursive:!0});const i=`${p}.${process.pid}.tmp`;return y.writeFileSync(i,JSON.stringify({key:r,...m,saved_at:new Date().toISOString()},null,2)+`
2
- `,{mode:384}),y.chmodSync(i,384),y.renameSync(i,p),g()}o(O,"saveLinearKey");function _(){return y.rmSync(f(),{force:!0}),g()}o(_,"removeLinearKey");function q({fetch:s}={}){const u=k();return u?{...$(u.key,{fetch:s}),saved:u}:null}o(q,"savedClient");export{d as LinearError,$ as linearClient,g as linearStatus,k as readLinearKey,_ as removeLinearKey,O as saveLinearKey,q as savedClient,I 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 i=(e,t)=>B(e,"name",{value:t,configurable:!0});import{ticketSource as R}from"../board/tickets.mjs";import{readManifest as g,withManifestLock as w,writeManifest as u}from"./boards.mjs";import{ROUTE_RE as v,routeFromName as D,routeRecord as h}from"./routes.mjs";import{repoService as E,validateGithub as I,validateBranch as P}from"./repos.mjs";import{STARTING_WAIT as x,startSession as F}from"./sessions.mjs";import{savedClient as L}from"./linear.mjs";const c=i((e,t=400)=>{throw Object.assign(new Error(e),{status:t})},"refuse");function M(e){(!e||typeof e!="object"||Array.isArray(e)||Object.keys(e).some(n=>!["name","github","branch","purpose","tickets"].includes(n)))&&c("Send only name, github, branch, purpose and tickets."),(typeof e.name!="string"||!e.name.trim()||e.name.length>60)&&c("Name must be 1\u201360 characters."),e.purpose!==void 0&&(typeof e.purpose!="string"||e.purpose.length>240)&&c("Purpose must be at most 240 characters.");const t=e.name.trim().replace(/\s+/g," "),r=D(t);v.test(r)||c("Name must contain a letter or digit for its route.");const a=I(e.github),m=P(e.branch);let d=e.tickets===void 0?{source:"internal"}:e.tickets,o;if(d?.source==="linear"&&typeof d=="object"&&Object.hasOwn(d,"new_project")){const{new_project:n,...b}=d;o=typeof n=="string"?n.trim():"",(!o||o.length>80)&&c("Name the new Linear project."),d={...b,project:o}}return{route:r,title:t,github:a,branch:m,tickets:R(d),newProject:o,purpose:(e.purpose||"").trim().replace(/\s+/g," ")}}i(M,"details");const S=i((e,t)=>Object.hasOwn(e.sites||{},t)||Object.hasOwn(e.starting||{},t)||Object.hasOwn(e.closed||{},t),"taken"),A="That board route already exists. Reopen it from Closed boards if it is closed.",N=i(e=>`'${e.replaceAll("'","'\\''")}'`,"quote"),q=i(({title:e,purpose:t})=>`--template board --title ${N(e)}${t?` --description ${N(t)}`:""}`,"joinFlags");function T(e){const t=String(e).split(`
2
- `).map(r=>r.trim()).find(Boolean)||"claude did not start";return t.length>200?`${t.slice(0,199)}\u2026`:t}i(T,"errorLine");async function _(e,t){await w(async()=>{const r=g(),a=h(r.starting,e);!a||h(r.sites,e)||(r.starting[e]=t.phase?{...a,phase:t.phase}:t.error?{...a,error:t.error}:{...a,session:t.session,label:t.name,since:new Date().toISOString(),repo:t.repo,...t.model?{model:t.model}:{}},await u(r))})}i(_,"recordStart");async function J(e,{run:t,live:r,repos:a=E(),changed:m=i(()=>{},"changed"),linear:d}={}){const o=M(e),{route:n,title:b,github:f,branch:y,newProject:j}=o;let{tickets:p}=o;const k=a.path(f),O=j===void 0?null:d||L();if(O){S(g(),n)&&c(A);try{p={...p,project:(await O.createProject(p.team,j)).id}}catch(s){c(s.message,s.status||502)}}await w(async()=>{const s=g();S(s,n)&&c(A),s.starting={...s.starting,[n]:{title:b,repo:k,github:f,branch:y,tickets:p,phase:a.cloned(f)?"starting":"cloning",asked_at:new Date().toISOString()}},await u(s)});const $=(async()=>{let s;try{await a.ensure(f,y),await _(n,{phase:"starting"}),m();const l=await F(n,{repo:k,prompt:q(o),run:t,live:r});s={session:l.session,name:l.name,repo:l.repo,model:l.model}}catch(l){s={error:T(l.message)}}try{await _(n,s)}catch(l){return{error:T(`could not record the start: ${l.message}`)}}return s.error?{error:s.error}:{session:s.session}})();return{route:n,started:$}}i(J,"requestBoard");async function Q(e){return w(async()=>{const t=g(),r=h(t.starting,e),a=Date.parse(r?.asked_at||r?.since),m=!!r?.error||Number.isFinite(a)&&Date.now()-a>=x;return(!r||h(t.sites,e)||!m)&&c("Only a board that didn\u2019t start can be dismissed."),delete t.starting[e],Object.keys(t.starting).length||delete t.starting,await u(t),{route:e}})}i(Q,"dismissBoard");export{Q as dismissBoard,J as requestBoard};
1
+ var $=Object.defineProperty;var a=(e,t)=>$(e,"name",{value:t,configurable:!0});import{ticketSource as x}from"../board/tickets.mjs";import{readManifest as b,withManifestLock as u,writeManifest as y}from"./boards.mjs";import{ROUTE_RE as D,routeFromName as v,routeRecord as j}from"./routes.mjs";import{repoService as k,validateGithub as A,validateBranch as B,validateRemote as L}from"./repos.mjs";import{startFailed as M,startSession as G,stopSession as C}from"./sessions.mjs";import{LINEAR_NEEDS_KEY as F,linearWorks as T,savedClient as q}from"./linear.mjs";const c=a((e,t=400)=>{throw Object.assign(new Error(e),{status:t})},"refuse");function H(e){(!e||typeof e!="object"||Array.isArray(e)||Object.keys(e).some(o=>!["name","github","remote","branch","purpose","tickets"].includes(o)))&&c("Send only name, github or remote, branch, purpose and tickets."),(typeof e.name!="string"||!e.name.trim()||e.name.length>60)&&c("Name must be 1\u201360 characters."),e.purpose!==void 0&&(typeof e.purpose!="string"||e.purpose.length>240)&&c("Purpose must be at most 240 characters.");const t=e.name.trim().replace(/\s+/g," "),s=v(t);D.test(s)||c("Name must contain a letter or digit for its route.");const{github:r,remote:d}=K(e),m=B(e.branch);let n=e.tickets===void 0?{source:"internal"}:e.tickets;d&&n?.source==="github"&&c("GitHub issues need a repo on GitHub; pick Internal or Linear.");let i;if(n?.source==="linear"&&typeof n=="object"&&Object.hasOwn(n,"new_project")){const{new_project:o,...f}=n;i=typeof o=="string"?o.trim():"",(!i||i.length>80)&&c("Name the new Linear project."),n={...f,project:i}}return{route:s,title:t,github:r,remote:d,branch:m,tickets:x(n),newProject:i,purpose:(e.purpose||"").trim().replace(/\s+/g," ")}}a(H,"details");function K(e){return e.github===void 0==(e.remote===void 0)&&c("Send the repo as github or as remote."),e.github!==void 0?{github:A(e.github)}:{remote:L(e.remote).remote}}a(K,"repoInput");const E=a((e,t)=>Object.hasOwn(e.sites||{},t)||Object.hasOwn(e.starting||{},t)||Object.hasOwn(e.closed||{},t),"taken"),R="That board route already exists. Reopen it from Closed boards if it is closed.",Y=a(e=>`'${e.replaceAll("'","'\\''")}'`,"quote"),U=a(({purpose:e})=>`--template board --items data/wave.json${e?` --description ${Y(e)}`:""}`,"joinFlags");function N(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}a(N,"errorLine");async function _(e,t){await u(async()=>{const s=b(),r=j(s.starting,e);!r||j(s.sites,e)||t.phase&&t.phase===r.phase||(s.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 y(s))})}a(_,"recordStart");const I=a(({title:e,purpose:t,repo:s,github:r,remote:d,branch:m,tickets:n},i)=>({title:e,purpose:t,repo:s,...r?{github:r}:{remote:d},branch:m,tickets:n,phase:i.cloned(r||d)?"starting":"cloning",asked_at:new Date().toISOString()}),"startingRecord"),S=new Map,te=a(e=>S.get(e),"clonePercent"),V=1e3;async function P(e,{purpose:t,repo:s,github:r,remote:d,branch:m},{run:n,live:i,repos:o,changed:f}){let p,g=-1/0;const w=a(l=>{S.set(e,l),!(Date.now()-g<V)&&(g=Date.now(),f())},"progress");try{try{await o.ensure(r||d,m,{progress:w})}finally{S.delete(e)}await _(e,{phase:"starting"}),f();const l=await G(e,{repo:s,prompt:U({purpose:t}),run:n,live:i,clones:o.base});p={session:l.session,name:l.name,repo:l.repo,model:l.model}}catch(l){p={error:N(l.message)}}try{await _(e,p)}catch(l){return{error:N(`could not record the start: ${l.message}`)}}return p.error?{error:p.error}:{session:p.session}}a(P,"start");async function re(e,{run:t,live:s,repos:r=k(),changed:d=a(()=>{},"changed"),linear:m}={}){const n=H(e),{route:i,github:o,remote:f,newProject:p}=n;let{tickets:g}=n;g.source==="linear"&&!m&&!T()&&c(F);const w=r.path(o||f),l=p===void 0?null:m||q();if(l){E(b(),i)&&c(R);try{g={...g,project:(await l.createProject(g.team,p)).id}}catch(h){c(h.message,h.status||502)}}return await u(async()=>{const h=b();E(h,i)&&c(R),h.starting={...h.starting,[i]:I({...n,repo:w,tickets:g},r)},await y(h)}),{route:i,started:P(i,{...n,repo:w},{run:t,live:s,repos:r,changed:d})}}a(re,"requestBoard");function O(e,t,s){const r=j(e.starting,t);return(!r||j(e.sites,t)||!M(r))&&c(`Only a board that didn\u2019t start can be ${s}.`),r}a(O,"failedStart");async function se(e,{run:t,live:s,stop:r,repos:d=k(),changed:m=a(()=>{},"changed")}={}){const n=O(b(),e,"retried");n.branch||c("Only a board asked for with New board can be retried; make it again with New board.");try{await C(n,{live:s,stop:r})}catch(o){c(`Couldn\u2019t stop its earlier session: ${o.message}`,502)}const i=await u(async()=>{const o=b(),f=O(o,e,"retried");return o.starting[e]=I({...f,repo:d.path(f.github||f.remote),purpose:f.purpose||""},d),await y(o),o.starting[e]});return{route:e,started:P(e,i,{run:t,live:s,repos:d,changed:m})}}a(se,"retryBoard");async function ne(e){return u(async()=>{const t=b();return O(t,e,"dismissed"),delete t.starting[e],Object.keys(t.starting).length||delete t.starting,await y(t),{route:e}})}a(ne,"dismissBoard");export{te as clonePercent,ne as dismissBoard,K as repoInput,re as requestBoard,se as retryBoard};
@@ -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(()=>{});
@@ -0,0 +1,3 @@
1
+ var y=Object.defineProperty;var u=(t,n)=>y(t,"name",{value:n,configurable:!0});import{execFile as h}from"node:child_process";const _="state,isDraft,reviewDecision,mergeable,mergeStateStatus,headRefOid,statusCheckRollup",f="/opt/homebrew/bin:/usr/local/bin:/usr/bin:/bin:/usr/sbin:/sbin",g=u(t=>String(t).trim().split(/\r?\n/)[0].slice(0,200),"firstLine");function d(t,n=process.env){return new Promise((s,o)=>{h("gh",t,{env:{...n,PATH:[n.PATH,f].filter(Boolean).join(":")},timeout:9e4,maxBuffer:16*1024*1024},(l,i,m)=>{if(l)return o(new Error(g(m||l.message)));try{s(JSON.parse(i))}catch(a){o(new Error(g(`unreadable output (${a.message})`)))}})})}u(d,"gh");const R="\u{1F916} ",b="<!-- claude-bridge -->";async function x({repo:t,pr:n,replyTo:s,words:o,login:l}){let i=process.env;if(l){const a=await new Promise(r=>h("gh",["auth","token","-u",l],{env:{...i,PATH:[i.PATH,f].filter(Boolean).join(":")}},(c,e)=>r(c?"":e.trim())));if(!a)throw new Error(`gh has no ${l} account; run gh auth login`);i={...i,GH_TOKEN:a}}const m=s===void 0?`repos/${t}/issues/${n}/comments`:`repos/${t}/pulls/${n}/comments/${s}/replies`;try{return(await d(["api",m,"-f",`body=${R}${o}
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 V=Object.defineProperty;var i=(t,e)=>V(t,"name",{value:e,configurable:!0});import m from"node:crypto";import c from"node:fs";import S from"node:path";import v from"node:os";import{validate as X,pollingOf as K,flushAt as G,hold as _,windowAt as Y,notifications as Z,snoozeEnd as tt}from"./schedule.mjs";import{presence as q}from"./presence.mjs";import{CODEX_SHARE_VALUES as et,codexStatus as C,effectiveCodexShare as nt}from"../shared/codex.mjs";const b=S.join(v.homedir(),".claude","kickflip","push"),z=S.join(b,"vapid.json"),F=S.join(b,"subs.json"),h=S.join(b,"queue.json"),A=h+".lock",J=i(t=>S.join(t,".claude","kickflip","settings.json"),"settingsFile"),P=i(t=>Buffer.from(t).toString("base64url"),"b64u"),$=i(t=>Buffer.from(t,"base64url"),"unb64u"),E=i((t,e)=>m.createHmac("sha256",t).update(e).digest(),"hmac"),l=i((t,e)=>{c.mkdirSync(b,{recursive:!0,mode:448});const n=`${t}.${process.pid}.tmp`;c.writeFileSync(n,JSON.stringify(e,null,2),{mode:384}),c.renameSync(n,t)},"writePrivate");function T(){try{return JSON.parse(c.readFileSync(z,"utf8"))}catch{}const t=m.generateKeyPairSync("ec",{namedCurve:"prime256v1"}).privateKey.export({format:"jwk"});return l(z,t),t}i(T,"vapidKey");function ot(){const t=T();return P(Buffer.concat([Buffer.from([4]),$(t.x),$(t.y)]))}i(ot,"publicKey");function g(){try{return JSON.parse(c.readFileSync(F,"utf8"))}catch{return[]}}i(g,"readSubs");function St(t,e,n=""){if(!/^https:\/\//.test(t?.endpoint||"")||!t.keys?.p256dh||!t.keys?.auth)throw Object.assign(new Error("bad subscription"),{status:400});const r=g().filter(s=>s.endpoint!==t.endpoint);return r.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(F,r),r.length}i(St,"addSubscription");function kt(){return g().map(t=>({host:new URL(t.endpoint).host,agent:t.agent,added:t.added}))}i(kt,"subscriptions");const R=i(t=>!t.agent||/iPhone|iPad|Android/.test(t.agent),"onPhone");async function L(t,e=!1){const n=g();if(t!=="action"||e||!(await q()).atMac)return n;const r=n.filter(s=>!R(s));return r.length?r:n}i(L,"reach");async function xt(){return{atMac:(await q()).atMac,desktops:g().filter(t=>!R(t)).length}}i(xt,"pushRoute");function rt(t,e){const n=$(t.keys.p256dh),r=m.createECDH("prime256v1"),s=r.generateKeys(),o=E($(t.keys.auth),r.computeSecret(n)),a=E(o,Buffer.concat([Buffer.from("WebPush: info\0"),n,s,Buffer.from([1])])),d=m.randomBytes(16),f=E(d,a),w=E(f,Buffer.from("Content-Encoding: aes128gcm\0")).subarray(0,16),k=E(f,Buffer.from("Content-Encoding: nonce\0")).subarray(0,12),x=m.createCipheriv("aes-128-gcm",w,k),O=Buffer.concat([x.update(Buffer.concat([Buffer.from(e),Buffer.from([2])])),x.final(),x.getAuthTag()]),u=Buffer.alloc(4);return u.writeUInt32BE(4096),Buffer.concat([d,u,Buffer.from([s.length]),s,O])}i(rt,"encrypt");function st(t){const e=T(),n=P(JSON.stringify({typ:"JWT",alg:"ES256"})),r=P(JSON.stringify({aud:new URL(t.endpoint).origin,exp:Math.floor(Date.now()/1e3)+12*3600,sub:t.origin||"mailto:kickflip@localhost"})),s=m.sign("sha256",Buffer.from(`${n}.${r}`),{key:m.createPrivateKey({key:e,format:"jwk"}),dsaEncoding:"ieee-p1363"});return`vapid t=${n}.${r}.${P(s)}, k=${ot()}`}i(st,"authorization");async function it(){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}=await t.json();return Number.isInteger(e)?e:void 0}catch{return}}i(it,"badgeTotal");const bt=i(t=>N(g(),t),"sendAll");async function N(t,e){if(!t.length)return[];const n=JSON.stringify({...e,badge:await it()}),r=await Promise.all(t.map(async o=>{try{const a=await fetch(o.endpoint,{method:"POST",headers:{authorization:st(o),"content-encoding":"aes128gcm","content-type":"application/octet-stream",ttl:"86400",urgency:"high"},body:rt(o,n),signal:AbortSignal.timeout(15e3)});return{sub:o,status:a.status,error:a.ok?void 0:(await a.text()).slice(0,300)}}catch(a){return{sub:o,status:0,error:String(a.message||a)}}})),s=new Set(r.filter(o=>o.status===404||o.status===410).map(o=>o.sub.endpoint));return s.size&&l(F,g().filter(o=>!s.has(o.endpoint))),r.map(o=>({host:new URL(o.sub.endpoint).host,agent:o.sub.agent,status:o.status,error:o.error,dropped:s.has(o.sub.endpoint)||void 0}))}i(N,"sendTo");const Q=i(t=>t.some(e=>e.status>=200&&e.status<300),"delivered");function j(t=v.homedir()){try{const e=JSON.parse(c.readFileSync(J(t),"utf8"));return e&&typeof e=="object"&&!Array.isArray(e)?e:{}}catch{return{}}}i(j,"readSettingsFile");function U(t,e=v.homedir()){const n=J(e);c.mkdirSync(S.dirname(n),{recursive:!0});const r=`${n}.${process.pid}.tmp`;c.writeFileSync(r,JSON.stringify(t,null,2)+`
2
- `),c.renameSync(r,n)}i(U,"writeSettingsFile");const Et=i((t=C(),e=v.homedir())=>nt(j(e).codexShare,t),"readCodexShare");function jt(t,e=C()){if(!et.includes(t))throw Object.assign(new Error("codex-share takes auto, always, or off"),{status:400});if(!e.usable)throw Object.assign(new Error(`${e.words} Run ${e.fix}, then choose again.`),{status:409});return U({...j(),codexShare:t}),t}i(jt,"setCodexShare");function D(){return K(j())}i(D,"readSettings");function Ot(){const t=j();if(!t.push)return null;const{push:e,...n}=t,{notifications:r,...s}=K(t);return U({...n,polling:s}),s}i(Ot,"migrateSettings");function at(t){const{settings:e,error:n}=X({notifications:D().notifications,...t});if(n)throw Object.assign(new Error(n),{status:400});const{push:r,...s}=j(),{notifications:o,...a}=e;return U({...s,polling:a,notifications:o}),e}i(at,"saveSettings");async function Bt(t,e=new Date){const n=D(),r={...n.notifications};if("on"in t&&(r.on=t.on),"weekends_off"in t&&(r.weekends_off=t.weekends_off),"snooze"in t){const a=t.snooze==="off"?null:tt(e,t.snooze,n);if(a===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 off."),{status:400});r.snoozed_until=a&&a.toISOString()}const{notifications:s}=at({...n,notifications:r}),o=s.on?0:await y(()=>{const{items:a}=p();return a.length&&l(h,{items:[]}),a.length});return{notifications:s,dropped:o}}i(Bt,"setNotifications");function p(){try{const t=JSON.parse(c.readFileSync(h,"utf8"));return{items:Array.isArray(t.items)?t.items:[]}}catch{return{items:[]}}}i(p,"readQueue");async function y(t){c.mkdirSync(b,{recursive:!0,mode:448});const e=Date.now()+1e4;for(;;)try{c.mkdirSync(A);break}catch(n){if(n.code!=="EEXIST")throw n;try{if(Date.now()-c.statSync(A).mtimeMs>3e4){c.rmdirSync(A);continue}}catch{}if(Date.now()>e)throw new Error("push queue is locked; try again");await new Promise(r=>setTimeout(r,50))}try{return t()}finally{try{c.rmdirSync(A)}catch{}}}i(y,"withQueueLock");function ct(t=new Date){const e=D(),{items:n}=p(),r=_(t,e);return{settings:e,window:Y(t,e),held:r!==null,hold:r,count:n.length,nextFlushAt:n.length?G(t,e):null}}i(ct,"queueStatus");const M=["action","prompt","fault"];class ut extends Error{static{i(this,"PushKindError")}}const ft=i(t=>M.includes(t)?null:`a push needs --kind ${M.join("|")}${t===void 0||t===!0?"":`, not "${t}"`}; news goes on the board, not in a push`,"pushKindError");async function vt(t,{kind:e,urgent:n=!1,immediate:r=!1,at:s=new Date}={}){const o=ft(e);if(o)throw new ut(o);if(!g().length)return{noDevice:!0};const a={title:t.title,body:t.body||"",url:t.url||"/",tag:t.tag||void 0},d=_(s,D());if(d?.why==="off")return{dropped:!0};const f=d!==null;let w=!1;if(n||r&&!f){const u=await N(await L(e,n),a);if(Q(u))return a.tag&&await y(()=>l(h,{items:p().items.filter(B=>B.tag!==a.tag)})),{sent:u};if(n)return{sent:u};w=!0}const k={id:m.randomUUID(),...a,kind:e,queuedAt:s.toISOString()},x=await y(()=>{const u=p(),B=k.tag?u.items.filter(W=>W.tag!==k.tag):u.items,H=B.length<u.items.length;return u.items=[...B,k],l(h,u),{replaced:H}}),O=ct(s);return{queued:{...x,count:O.count,nextFlushAt:O.nextFlushAt,held:f,hold:d,sendFailed:w}}}i(vt,"deliver");async function At(t,e,n){return!t||!p().items.some(r=>r.tag===t)?null:y(()=>{const r=p(),s=r.items.find(o=>o.tag===t);return s?(s.body=e(s.body||""),n&&(s.url=n),l(h,r),s.body):null})}i(At,"reviseQueued");async function Pt(t,e){const n=new Set(t),r=i(s=>n.has(s.tag)&&typeof s.url=="string"&&s.url.startsWith(`/${e}/`),"matches");return p().items.some(r)?y(()=>{const{items:s}=p(),o=s.filter(d=>!r(d)),a=s.length-o.length;return a&&l(h,{items:o}),a}):0}i(Pt,"dropQueued");let I=null;function $t(){return I||=(async()=>{const t=await y(()=>{const o=p();return o.items.length&&l(h,{items:[]}),o.items});if(!t.length)return null;const e=[],n=[],r=[],s=new Map(t.map(o=>[o.id,o.kind]));for(const o of Z(t)){const a=await N(await L(s.get(o.id)),{title:o.title,body:o.body,url:o.url,tag:o.tag});r.push(...a),(Q(a)?e:n).push(o)}return n.length&&await y(()=>{const o=p(),a=new Set(o.items.map(f=>f.tag).filter(Boolean)),d=t.filter(f=>n.some(w=>w.id===f.id)&&!(f.tag&&a.has(f.tag)));l(h,{items:[...d,...o.items]})}),{count:t.length,sent:e.length,ok:!n.length,results:r}})().finally(()=>{I=null}),I}i($t,"flushQueue");export{M as PUSH_KINDS,ut as PushKindError,St as addSubscription,vt as deliver,Pt as dropQueued,$t as flushQueue,Ot as migrateSettings,ot as publicKey,ft as pushKindError,xt as pushRoute,ct as queueStatus,Et as readCodexShare,D as readSettings,j as readSettingsFile,At as reviseQueued,at as saveSettings,bt as sendAll,jt as setCodexShare,Bt as setNotifications,kt 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};
@@ -0,0 +1,2 @@
1
+ var T=Object.defineProperty;var i=(t,r)=>T(t,"name",{value:r,configurable:!0});import f from"node:fs";import m from"node:os";import y from"node:path";import{execFile as N}from"node:child_process";import{githubLogin as P}from"./pr-report.mjs";import{codexStatusReader as w}from"../shared/codex.mjs";import{linearStatus as R}from"./linear.mjs";import{readSettings as O,subscriptions as C}from"./push.mjs";import{tailscaleState as I,tailscaleInstallHint as L}from"../bin/install.mjs";const x=[["github","GitHub","GitHub polling and PR reports wait on this."],["codex","Codex","Codex implementing and reviewing wait on this; Codex share stays Off."],["linear","Linear",null],["slack","Slack","Reviewer DMs and digests wait on this."],["tailscale","Tailscale","Try builds wait on this."],["notifications","Notifications","Pushes wait on this."]],k=i(t=>y.join(t,".claude","kickflip","integrations.json"),"reportFile");function F(t=m.homedir()){try{return JSON.parse(f.readFileSync(k(t),"utf8")).slack||null}catch{return null}}i(F,"readSlackReport");function Q(t,{home:r=m.homedir(),now:s=new Date,route:d=null,title:o="Board"}={}){const e=k(r),n={ready:t,at:s.toISOString(),route:d,title:o};f.mkdirSync(y.dirname(e),{recursive:!0});const a=`${e}.${process.pid}.tmp`;return f.writeFileSync(a,JSON.stringify({slack:n},null,2)+`
2
+ `,{mode:384}),f.renameSync(a,e),n}i(Q,"reportSlack");const H=i((t,r)=>{const s=Math.floor((r-Date.parse(t))/6e4);return s<1?"just now":s<60?`${s} min ago`:s<1440?`${Math.floor(s/60)} h ago`:`${Math.floor(s/1440)} days ago`},"age"),u=i((t,r,s)=>({settings:t,label:`Hub settings \u203A ${r}`,text:s}),"settingsFix");function $(t,{now:r=Date.now(),platform:s=process.platform}={}){const d=i((o,e)=>{switch(o){case"github":return e?{ready:!0,detail:`gh logged in as ${e}`}:{ready:!1,missing:"gh isn't logged in on the host",fix:{command:"gh auth login"}};case"codex":return e.usable?{ready:!0,detail:"Installed and logged in"}:{ready:!1,missing:e.words,fix:{command:e.fix}};case"linear":return e.set?e.refused?{ready:!1,missing:"Linear refused the key",fix:u("linear","Linear","Paste a new one in")}:{ready:!0,detail:`Key set \xB7 ${e.workspace} workspace`}:{ready:!1,missing:"No Linear key set",fix:u("linear","Linear","Add one in")};case"slack":{const n=e?`${e.title}\u2019s session reported ${e.ready?"a":"no"} Slack connector, ${H(e.at,r)}`:"No board session has reported yet";return e?.ready?{ready:!0,detail:n}:{ready:!1,missing:n,fix:{text:"Add Slack to Claude\u2019s connectors. The next board session to start reports it."}}}case"tailscale":{let n,a;return e.installed?e.signedIn?e.pointsAtHub?e.operatorChecked&&e.operator!==e.username&&(n=`Tailscale operator is ${e.operator||"none"} (expected ${e.username})`,a="sudo tailscale set --operator=$USER"):(n="Tailscale / is not serving the hub",a="kickflip doctor --fix"):(n="Tailscale isn't signed in",a="tailscale up"):(n="Tailscale isn't installed on the host",a=L(s)),n?{ready:!1,missing:n,fix:{command:a}}:{ready:!0,detail:`Serving ${e.dns}${e.operatorChecked?" \xB7 operator "+e.operator:""}`}}case"notifications":return e.on&&e.devices?{ready:!0,detail:`Pushes on \xB7 ${e.devices} device${e.devices===1?"":"s"}`}:{ready:!1,missing:e.on?"No device has turned on notifications":"Pushes are off",fix:u("notifications","Notifications","Turn on notifications in")}}},"row");return x.filter(([o])=>Object.hasOwn(t,o)).map(([o,e,n])=>({id:o,name:e,...d(o,t[o]),waits:n}))}i($,"readinessRows");const j=i(t=>(r,s)=>new Promise(d=>{N(r,s,{env:t,timeout:3e3},(o,e)=>d({status:o?1:0,stdout:e}))}),"asyncRun");function D({home:t=m.homedir(),env:r=process.env,now:s=Date.now,cacheMs:d=6e4,probes:o={}}={}){const e=w({home:t,env:r,now:s,cacheMs:d}),n={github:i(()=>P({env:r}).catch(()=>null),"github"),codex:e,linear:i(()=>R(t),"linear"),slack:i(()=>F(t),"slack"),tailscale:i(()=>I({run:j(r),port:Number(r.KICKFLIP_HUB_PORT||4500)}),"tailscale"),notifications:i(()=>({on:O(t).notifications.on,devices:C(t).length}),"notifications"),...o},a=new Map;async function g(l,c){if(!["github","tailscale"].includes(l))return n[l]({fresh:c});const p=a.get(l);if(!c&&p&&s()-p.at<d)return p.value;const h=Promise.resolve().then(()=>n[l]());return a.set(l,{at:s(),value:h}),h}i(g,"check");const b=i(async({fresh:l=!1}={})=>$(Object.fromEntries(await Promise.all(x.map(async([c])=>[c,await g(c,l)]))),{now:s()}),"rows");async function S(l){const[c]=$({[l]:await g(l,!1)},{now:s()});return c.ready||!c.waits?null:c.waits.replace("this",`${c.name}: ${c.missing.replace(/\.$/,"")}`)}return i(S,"waiting"),{rows:b,waiting:S}}i(D,"readinessReader");const A=w(),V=D({probes:{codex:A}});export{A as readCodex,F as readSlackReport,V as readiness,D as readinessReader,$ as readinessRows,Q as reportSlack};