@lovek4d/kickflip 0.4.0 → 0.5.3

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