@lovek4d/kickflip 0.0.0-stage → 0.3.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (127) hide show
  1. package/CONTEXT.md +513 -0
  2. package/LICENSE +57 -0
  3. package/README.md +64 -2
  4. package/SKILL.md +142 -0
  5. package/bin/doctor.mjs +4 -0
  6. package/bin/install.mjs +3 -0
  7. package/bin/kickflip.mjs +180 -0
  8. package/bin/statusline.mjs +5 -0
  9. package/bin/wait-events.mjs +3 -0
  10. package/board/arch-review.mjs +5 -0
  11. package/board/bin/agent.mjs +2 -0
  12. package/board/bin/board.mjs +379 -0
  13. package/board/bin/builder-stop-hook.mjs +6 -0
  14. package/board/build-script.mjs +1 -0
  15. package/board/domain.mjs +5 -0
  16. package/board/on-host.mjs +1 -0
  17. package/board/public/app-arch.js +75 -0
  18. package/board/public/app-asks.js +188 -0
  19. package/board/public/app-banner.js +48 -0
  20. package/board/public/app-cards.js +393 -0
  21. package/board/public/app-core.js +130 -0
  22. package/board/public/app-header.js +126 -0
  23. package/board/public/app-lanes.js +142 -0
  24. package/board/public/app-needs.js +31 -0
  25. package/board/public/app-snapshot.js +131 -0
  26. package/board/public/app-threads.js +167 -0
  27. package/board/public/app.html +86 -0
  28. package/board/public/app.js +438 -0
  29. package/board/public/attach.js +84 -0
  30. package/board/public/dialogs.js +358 -0
  31. package/board/public/hub-kit.js +168 -0
  32. package/board/public/hub.css +170 -0
  33. package/board/public/kickflip.png +0 -0
  34. package/board/public/linear-key.js +141 -0
  35. package/board/public/prompts.js +64 -0
  36. package/board/public/prototype-kit.js +193 -0
  37. package/board/public/prototype-switcher.js +226 -0
  38. package/board/public/prototype.html +55 -0
  39. package/board/public/router.js +226 -0
  40. package/board/public/rules-editor.js +127 -0
  41. package/board/public/shell.js +298 -0
  42. package/board/public/sidebar.css +219 -0
  43. package/board/public/sidebar.js +359 -0
  44. package/board/public/style.css +665 -0
  45. package/board/public/toasts.js +181 -0
  46. package/board/public/ui-cards.js +218 -0
  47. package/board/public/ui-enums.js +10 -0
  48. package/board/public/ui-header.js +93 -0
  49. package/board/public/ui-hub.js +112 -0
  50. package/board/public/ui-links.js +150 -0
  51. package/board/public/ui-markdown.js +80 -0
  52. package/board/public/ui-order.js +18 -0
  53. package/board/public/ui-page-header.js +65 -0
  54. package/board/public/ui-prompts.js +39 -0
  55. package/board/public/ui-questions.js +151 -0
  56. package/board/public/ui-sidebar.js +215 -0
  57. package/board/public/ui-snapshot.js +62 -0
  58. package/board/public/ui-threads.js +90 -0
  59. package/board/public/ui.js +247 -0
  60. package/board/public/view-frame.js +93 -0
  61. package/board/public/view-hub-settings.js +282 -0
  62. package/board/public/view-hub.js +150 -0
  63. package/board/public/view-notes.js +32 -0
  64. package/board/public/view-polling.js +87 -0
  65. package/board/public/view-questions.js +411 -0
  66. package/board/public/view-settings.js +383 -0
  67. package/board/review.mjs +1 -0
  68. package/board/server.mjs +17 -0
  69. package/board/store.mjs +15 -0
  70. package/board/tickets.mjs +3 -0
  71. package/board/worktree.mjs +15 -0
  72. package/docs/hub-session.md +11 -0
  73. package/hooks-plugin/.claude-plugin/plugin.json +5 -0
  74. package/hooks-plugin/hooks/codex-share-auto.mjs +23 -0
  75. package/hooks-plugin/hooks/hooks.json +54 -0
  76. package/hooks-plugin/hooks/usage-gate.mjs +18 -0
  77. package/hooks-plugin/hooks/watcher-stop.mjs +41 -0
  78. package/hub/awake.mjs +1 -0
  79. package/hub/board-repo.mjs +1 -0
  80. package/hub/boards.mjs +4 -0
  81. package/hub/close-board.mjs +1 -0
  82. package/hub/duration.mjs +1 -0
  83. package/hub/files.mjs +1 -0
  84. package/hub/github.mjs +4 -0
  85. package/hub/home.mjs +1 -0
  86. package/hub/hub.mjs +19 -0
  87. package/hub/icon-180.png +0 -0
  88. package/hub/icon-192.png +0 -0
  89. package/hub/icon-512.png +0 -0
  90. package/hub/icons.mjs +1 -0
  91. package/hub/linear.mjs +2 -0
  92. package/hub/live.mjs +2 -0
  93. package/hub/locks.mjs +1 -0
  94. package/hub/new-board.mjs +2 -0
  95. package/hub/permission-hook.mjs +3 -0
  96. package/hub/pr-report.mjs +1 -0
  97. package/hub/presence.mjs +1 -0
  98. package/hub/prompts.mjs +2 -0
  99. package/hub/push.mjs +2 -0
  100. package/hub/readiness.mjs +2 -0
  101. package/hub/repos.mjs +3 -0
  102. package/hub/restart-board.mjs +1 -0
  103. package/hub/routes.mjs +1 -0
  104. package/hub/rules.mjs +1 -0
  105. package/hub/schedule.mjs +1 -0
  106. package/hub/sessions.mjs +2 -0
  107. package/hub/shell.mjs +2 -0
  108. package/hub/slack.mjs +2 -0
  109. package/hub/stream.mjs +19 -0
  110. package/hub/sw.js +144 -0
  111. package/hub/trust.mjs +2 -0
  112. package/hub/try.mjs +4 -0
  113. package/hub/upgrade.mjs +1 -0
  114. package/hub/usage.mjs +3 -0
  115. package/hub/views.mjs +1 -0
  116. package/package.json +26 -4
  117. package/shared/codex.mjs +1 -0
  118. package/shared/colors.css +15 -0
  119. package/shared/fake-gh.mjs +54 -0
  120. package/shared/repos.mjs +1 -0
  121. package/shared/service.mjs +33 -0
  122. package/shared/skills.mjs +2 -0
  123. package/shared/usage-gate.mjs +1 -0
  124. package/starter/cross-site.mjs +1 -0
  125. package/starter/public/claude.js +68 -0
  126. package/starter/public/index.html +49 -0
  127. package/starter/server.mjs +4 -0
package/hub/hub.mjs ADDED
@@ -0,0 +1,19 @@
1
+ #!/usr/bin/env node
2
+ var $t=Object.defineProperty;var c=(e,t)=>$t(e,"name",{value:t,configurable:!0});import Te from"node:http";import Oe from"node:net";import{spawn as je,execFileSync as _t}from"node:child_process";import g from"node:fs";import k from"node:path";import Pe from"node:os";import{fileURLToPath as xt}from"node:url";import Re from"node:vm";import vt from"node:zlib";import{routeRecord as j}from"./routes.mjs";import{publicKey as Tt,addSubscription as Ot,removeSubscription as jt,sendAll as Pt,flushQueue as Ee,queueStatus as le,saveSettings as Rt,readSettings as A,migrateSettings as Et,setNotifications as Dt,readCodexShare as De,setCodexShare as Lt,readClaudeModel as Ct,setClaudeModel as Ht,deliver as At,pushRoute as It}from"./push.mjs";import{POLL_MS as Le,answerOf as Mt,expired as Bt,pendingFor as Ce,promptRecord as Ft,pushOf as Ut,updatePrompts as I,shown as Nt,pruneRemembered as de,rememberRules as Gt,rememberedAnswer as zt,rememberedFor as He}from"./prompts.mjs";import{nextSlot as Kt,windowAt as Wt,hold as Jt,heldUntil as Vt,holdLine as Zt,editPolling as Xt}from"./schedule.mjs";import{capital as Qt,hubView as Yt,settingsView as qt,pollingView as en,flushView as tn,SNOOZE_REFUSED as nn,AWAKE_REFUSED as an}from"./views.mjs";import{caffeinate as sn,setKeepAwake as rn,awakeUntil as Ae,awakeUnsupported as on}from"./awake.mjs";import{checkGitHub as cn,sendWakes as un}from"./github.mjs";import{listBoards as R,isBoardSite as ln,listenState as Ie,RESUME_STATE as dn,sendToBoards as Me,checkListening as hn,recordHubCode as fn,runningHubCode as pn,recordLinkFaults as mn,linksOf as gn,hubLink as wn,hubCodeState as bn,readJSON as yn}from"./boards.mjs";import{liveSessions as Be}from"./live.mjs";import{RuleError as Sn,applyRules as kn,changed as Fe,readHubRules as he,writeHubRules as $n}from"./rules.mjs";import{siteIcon as _n}from"./icons.mjs";import{readNeeds as xn,sidebarSites as vn,startRoute as Tn,hubHealth as On}from"./home.mjs";import{checkSessions as jn,startingBoards as Pn}from"./sessions.mjs";import{repoService as Rn,repoLookupRoute as En}from"./repos.mjs";import{boardRepo as Dn,changeBoardRepo as Ln,migrateBoardRepos as Cn,migrateHubBoard as Hn}from"./board-repo.mjs";import{requestBoard as An,retryBoard as In,dismissBoard as Mn}from"./new-board.mjs";import{boardClosure as Bn,closeBoard as Fn,closedBoards as Un,reopenBoard as Nn}from"./close-board.mjs";import{restartBoard as Gn,restartable as zn}from"./restart-board.mjs";import{crossSite as Kn,isJSON as fe}from"../starter/cross-site.mjs";import{BOARD_PUBLIC as pe,appFile as Wn,colorsVersion as Ue,destOf as Jn,fileVersion as Vn,pageOf as Ne,shellHtml as Zn}from"./shell.mjs";import{hubStream as Xn}from"./stream.mjs";import{usageReader as Qn,checkUsagePause as Yn,contextReading as qn}from"./usage.mjs";import{readUsageGate as Ge,setPauseAt as ea,setKeepGoing as ta}from"../shared/usage-gate.mjs";import{readiness as na,readCodex as W}from"./readiness.mjs";import{BuildError as aa,startBuild as sa,stopTries as ra,tryTick as ze,initializeTries as oa,fetchTryBranches as Ke}from"./try.mjs";import{serveFiles as ia}from"./files.mjs";import{upgrader as ca,CHECK_MS as ua}from"./upgrade.mjs";import{openBoardBuilders as la}from"../board/store.mjs";import{cloneRoot as We}from"../shared/repos.mjs";import{LinearError as me,linearStatus as J,removeLinearKey as da,saveLinearKey as ha,savedClient as Je}from"./linear.mjs";const V=k.dirname(xt(import.meta.url)),fa=k.join(V,"..","shared","colors.css"),u=c((...e)=>console.log(new Date().toISOString(),...e),"log"),Z=k.join(Pe.homedir(),".claude","kickflip"),Ve=k.join(Z,"manifest.json"),X=k.join(Z,"logs"),Ze=k.join(Z,"run"),ge=Number(process.env.KICKFLIP_HUB_PORT||4500);for(const e of[Z,X,Ze])g.mkdirSync(e,{recursive:!0});try{const e=fn();u(`hub code ${e.hash} (${e.files.length} files)`)}catch(e){u("could not record the hub code hash:",e.message)}try{const e=await Cn();e.length&&u(`manifest: recorded GitHub repos for ${e.map(t=>`/${t}`).join(", ")}`)}catch(e){u("board repo migration failed:",e.message)}try{const e=await Hn();e&&u(`manifest: /${e}, the hub board, builds from its own folder's local main`)}catch(e){u("hub board migration failed:",e.message)}const L=Rn(),pa=Qn(),Q=sn({log:u});Q.apply();let Y={version:1,sites:{}};function w(){try{Y=JSON.parse(g.readFileSync(Ve,"utf8")),Y.sites||={}}catch(e){e.code==="ENOENT"?Y={version:1,sites:{}}:u("manifest unreadable, keeping the last good copy:",e.message)}return Y}c(w,"loadManifest");const T=new Map,C=new Set;let q=!1;const Xe=5e3,ee=c(e=>JSON.stringify([e.cmd,e.cwd||e.dir,e.port,e.env||{},e.restart_at||0]),"keyOf"),we=c(e=>Array.isArray(e?.cmd)&&e.cmd.length>0&&!!e.port,"runs"),M=c(e=>we(e)?"cmd":e?.port?"port":"files","servesOf"),B=new Map;function te(e){return k.join(Ze,e+".pid")}c(te,"pidFile");function ma(e,t){try{const n=Number(g.readFileSync(te(e),"utf8")),r=n?_t("ps",["-o","command=","-p",String(n)],{encoding:"utf8"}).trim():"";r&&r.includes(t.cmd[t.cmd.length-1])&&(process.kill(-n,"SIGTERM"),u(`stopped stale ${e} (pgid ${n})`))}catch{}try{g.unlinkSync(te(e))}catch{}}c(ma,"killStale");function ga(e,t){const n=T.get(e)||{restarts:0},r=g.openSync(k.join(X,e+".log"),"a"),[a,...l]=t.cmd,h={...process.env,...t.env||{},PORT:String(t.port),PREFIX:"/"+e,KICKFLIP_ROUTE:e};g.writeSync(r,`
3
+ --- ${new Date().toISOString()} hub starting: ${t.cmd.join(" ")} (port ${t.port})
4
+ `);let i;try{i=je(a,l,{cwd:t.cwd||t.dir,env:h,stdio:["ignore",r,r],detached:!0})}catch(b){g.writeSync(r,`hub could not start it: ${b.message}
5
+ `),g.closeSync(r),Object.assign(n,{child:null,key:ee(t),state:"crashed",exitCode:null,nextStart:Date.now()+Qe(n.restarts++)}),T.set(e,n);return}g.closeSync(r),g.writeFileSync(te(e),String(i.pid)),Object.assign(n,{child:i,key:ee(t),state:"starting",since:Date.now(),exitCode:null,up:!1}),T.set(e,n),u(`started ${e} pid ${i.pid}`),i.pid&&C.add(i);const y=c((b,S)=>{if(C.delete(i),n.child!==i)return;try{g.unlinkSync(te(e))}catch{}Date.now()-n.since>6e4&&(n.restarts=0),Object.assign(n,{child:null,state:n.stopping?"stopped":"crashed",exitCode:b??S,nextStart:Date.now()+Qe(n.restarts++)}),n.stopping=!1,u(`${e} exited (${b??S})`)},"ended");i.on("error",b=>{u(`${e} spawn error: ${b.message}`),i.pid||y(b.code,null)}),i.on("exit",y)}c(ga,"start");const Qe=c(e=>Math.min(6e4,1e3*2**Math.min(e,6)),"backoff");function be(e){const t=T.get(e);if(!t||!t.child){T.delete(e);return}t.stopping=!0;const n=t.child;ne(n,"SIGTERM"),setTimeout(()=>{C.has(n)&&ne(n,"SIGKILL")},Xe)}c(be,"stop");const ne=c((e,t)=>{try{process.kill(-e.pid,t)}catch{}},"signal");function Ye(e){return new Promise(t=>{const n=Oe.connect({port:e,host:"127.0.0.1"}),r=c(a=>{n.destroy(),t(a)},"done");n.setTimeout(800,()=>r(!1)),n.on("connect",()=>r(!0)),n.on("error",()=>r(!1))})}c(Ye,"probe");let qe=!0;const F=new Set,ae=new Set,wa=3e4;async function ba(e){const t=Date.now()+wa;for(;;){const n=j(w().sites,e),r=T.get(e);if(n&&r?.child&&r.up&&r.key===ee(n))return!0;if(Date.now()>=t)return!1;await new Promise(a=>setTimeout(a,250))}}c(ba,"serverAnswers");async function et(){if(q)return;const{sites:e}=w();for(const t of[...T.keys()])F.has(t)||(j(e,t)?we(j(e,t))||(u(`${t} no longer has a command; stopping it`),be(t),T.delete(t)):(u(`${t} left the manifest; stopping it`),be(t),T.delete(t)));for(const t of[...B.keys()])M(j(e,t))!=="port"&&B.delete(t);for(const[t,n]of Object.entries(e))M(n)==="port"&&B.set(t,await Ye(n.port));for(const[t,n]of Object.entries(e)){if(!we(n))continue;qe&&ma(t,n);const r=T.get(t);if(r&&r.child&&r.key!==ee(n)){u(`${t} entry changed; restarting it`),be(t),r.restarts=0,r.nextStart=Date.now()+1500;continue}(!r||!r.child&&Date.now()>=(r.nextStart||0))&&ga(t,n)}qe=!1;for(const[t,n]of T){const r=j(e,t);r&&(n.up=n.child?await Ye(r.port):!1,n.child&&n.up&&(n.state="up"))}}c(et,"reconcile");const ya=setInterval(()=>et().catch(e=>u("reconcile failed:",e.message)),2e3);et().catch(e=>u("reconcile failed:",e.message));try{Et()&&u("settings: migrated the push schedule to the polling interval")}catch(e){u("settings migration failed:",e.message)}const Sa=6e4,tt=60*6e4,$={running:null,startedAt:0,firstAt:Date.now()+Sa,last:null,errorLoggedAt:0,linearErrorLoggedAt:0},nt=c(()=>new Date($.startedAt?Kt(new Date($.startedAt),A()).getTime():$.firstAt),"nextPollAt");function at({flush:e=!0}={}){if($.running)return $.running;const t=new Date;return $.startedAt=t.getTime(),$.running=(async()=>{const n=A();let r;try{r=await cn({every:Wt(t,n).every_minutes,manifest:w()})}catch(a){r={at:t.toISOString(),watched:0,failed:[a.message],changes:[],events:[],checked:[],merged:[],linear:{moved:[],failed:[]}}}r.merged.length&&(await Ke({manifest:w(),log:u,routes:r.merged}),u(`merged on ${r.merged.join(", ")}: fetched and refreshing Main`),ze({manifest:w(),routes:r.merged}).catch(a=>u("merge refresh failed:",a.message))),await Ke({manifest:w(),log:u}),r.failed.length&&Date.now()-$.errorLoggedAt>tt&&($.errorLoggedAt=Date.now(),u(`github check: ${r.failed.length} step${r.failed.length===1?"":"s"} failed, first: ${r.failed[0]} (logged at most hourly)`)),r.changes.length&&u(`github check: ${r.changes.map(a=>`#${a.pr} ${a.changes.join(",")}`).join("; ")} \u2192 ${r.events.map(a=>a.route).join(", ")||"no board"}`);for(const{id:a,state:l}of r.linear.moved)u(`linear: ${a} \u2192 ${l}`);r.linear.failed.length&&Date.now()-$.linearErrorLoggedAt>tt&&($.linearErrorLoggedAt=Date.now(),u(`linear check: ${r.linear.failed.length} step${r.linear.failed.length===1?"":"s"} failed, first: ${r.linear.failed[0]} (logged at most hourly)`));try{r.deaf=await hn({manifest:w()})}catch(a){r.deaf={pushed:[],failed:[a.message]}}(r.deaf.pushed.length||r.deaf.failed.length)&&u(`not listening: pushed ${r.deaf.pushed.join(", ")||"none"}${r.deaf.failed.length?`; ${r.deaf.failed.join("; ")}`:""}`);try{mn(w())}catch(a){u("could not record link faults:",a.message)}if(r.flushed=null,r.held=Vt(t,n)!==null,e&&!r.held)try{const a=await Ee();a&&(r.flushed={count:a.count,sent:a.sent,ok:a.ok},u(`push queue: sent ${a.sent} of ${a.count} queued`))}catch(a){u("push queue flush failed:",a.message)}return $.last=r,r})().finally(()=>{$.running=null}),$.running}c(at,"runPoll");let st=null;function ka(){st||=un({settings:A(),manifest:w()}).then(e=>{e.due.length&&u(`wake ${e.due.join(", ")}: check-github \u2192 ${e.events.map(t=>t.route).join(", ")||"no board"}${e.failed.length?`; ${e.failed.join("; ")}`:""}`)}).catch(e=>u("wake failed:",e.message)).finally(()=>{st=null})}c(ka,"runWakes");const ye=new Set,D={running:null};function rt(){if(D.running||F.size||ae.size)return;const e=new Set(ye);ye.clear(),D.running=jn({manifest:w(),asked:e}).then(t=>{(t.resumed.length||t.pushed.length||t.failed.length)&&u(`sessions: resumed ${t.resumed.join(", ")||"none"}; pushed ${t.pushed.join(", ")||"none"}${t.failed.length?`; ${t.failed.join("; ")}`:""}`)}).catch(t=>u("session check failed:",t.message)).finally(()=>{D.running=null})}c(rt,"runSessionCheck"),await oa(w());const $a=Date.now(),U=new Map;function N(e){e?(se.delete(e),_.changed(e)):_.refresh()}c(N,"promptChanged");async function ot(){const{gone:e,rulesChanged:t}=await I(n=>{const r=Bt(n,{now:Date.now(),aliveSince:$a}),a=de(n,Se());for(const l of r)delete n.prompts[l.id];return{gone:r,rulesChanged:a}});for(const n of e)N(n.route);t&&_.refresh(),e.length&&u(`prompts: dropped ${e.map(n=>n.id).join(", ")}, whose hook went away`)}c(ot,"prunePrompts");const Se=c(()=>({running:Be(),boards:R(w())}),"promptContext");let G=null,it=null;async function ke(){return G?it??=G.catch(()=>{}).then(()=>(it=null,ke())):(G=(async()=>Yn({gate:Ge(),resume:c(async e=>{const t=await Me(R(w()),"usage-resumed",{window:e});for(const n of t.failed)u(n)},"resume")}))().finally(()=>{G=null}),G)}c(ke,"checkUsageGate"),setInterval(()=>{ze({manifest:w()}).catch(e=>u("try tick failed:",e.message)),z.tick().catch(e=>u("kickflip update tick failed:",e.message)),ke().catch(e=>u("usage gate failed:",e.message)),!$.running&&Date.now()>=nt().getTime()&&at().catch(e=>u("poll failed:",e.message)),ka(),Q.apply(),ot().catch(e=>u("prompt expiry failed:",e.message)).finally(rt)},15e3);function ct(){if(q)return;q=!0,clearInterval(ya),Q.stop(),u("hub stopping; stopping sites");for(const n of T.values())n.child&&(n.stopping=!0);for(const n of C)ne(n,"SIGTERM");let e=!1;ra().finally(()=>{e=!0});const t=Date.now()+Xe;setInterval(()=>{if(!((!e||C.size)&&Date.now()<t)){for(const n of C)u(`pid ${n.pid} ignored SIGTERM; killing it`),ne(n,"SIGKILL");process.exit(0)}},50)}c(ct,"shutdown"),process.on("SIGTERM",ct),process.on("SIGINT",ct);const ut=new Map;function lt(e,t={n:3e3}){let n=0,r;try{r=g.readdirSync(e,{withFileTypes:!0})}catch{return 0}for(const a of r){if(t.n--<=0)break;if(a.name.startsWith(".")||a.name==="node_modules")continue;const l=k.join(e,a.name);if(a.isDirectory())n=Math.max(n,lt(l,t));else try{n=Math.max(n,g.statSync(l).mtimeMs)}catch{}}return n}c(lt,"newestMtime");function _a(e,t){const n=ut.get(e);if(n&&Date.now()-n.checked<3e4)return n.at;const r=lt(t.dir);return ut.set(e,{at:r,checked:Date.now()}),r}c(_a,"changedAt");const dt=c(e=>{try{return typeof e=="string"&&g.statSync(e).isDirectory()}catch{return!1}},"isFolder");function $e(){const{sites:e}=w();return Object.entries(e).sort(([t],[n])=>t.localeCompare(n)).map(([t,n])=>{const r=T.get(t)||{},a=M(n),l=a==="files"?dt(n.dir)?"up":"down":a==="port"?B.has(t)?B.get(t)?"up":"down":"starting":r.child?r.up?"up":"starting":r.state==="crashed"?"down":r.state||"starting";return{route:t,title:n.title||t,description:n.description||"",icon:n.icon||null,color:n.color||null,port:n.port??null,serves:a,state:l,exitCode:r.exitCode??null,owner:n.owner||null,restart:zn(n),changedAt:_a(t,n)||null,created:n.created||null,log:k.join(X,t+".log")}})}c($e,"status");const H=c(e=>String(e??"").replace(/[&<>"']/g,t=>({"&":"&amp;","<":"&lt;",">":"&gt;",'"':"&quot;","'":"&#39;"})[t]),"esc"),se=new Map,ht=2e4,xa=1500;async function va(e){try{const t=await fetch(`http://127.0.0.1:${e}/_claude/needs`,{signal:AbortSignal.timeout(xa)});return t.ok?xn(await t.json()):null}catch{return null}}c(va,"fetchNeeds");async function ft(e,{fresh:t=!1}={}){const n=e.filter(a=>a.state==="up"&&a.owner&&a.port);for(const a of n){const l=se.get(a.route);(t===!0||t?.has?.(a.route)||!l||Date.now()-l.at>=ht)&&se.set(a.route,{at:Date.now(),needs:va(a.port)})}const r=await Promise.all(n.map(a=>se.get(a.route).needs));return new Map(n.map((a,l)=>[a.route,r[l]]))}c(ft,"waitingCounts");const pt=c(e=>[...e.values()].reduce((t,n)=>t+(n?.count||0),0)+Ce(null).length,"badgeTotal"),_e=new Map;function Ta(e){const t=new Map,n=new Map,r={running:Be(),wakes:yn(dn,{})};for(const a of e)try{const l=Ie(a,r);n.set(a.route,l);let h=_e.get(a.route);(!h||h.listen!==l||Date.now()-h.at>=ht)&&(h={at:Date.now(),listen:l,links:gn(a,{listen:l})},_e.set(a.route,h)),t.set(a.route,h.links)}catch(l){u(`links of ${a.route} failed:`,l.message),t.set(a.route,[])}return{links:t,listens:n}}c(Ta,"boardLinks");async function re({fresh:e=!1}={}){const t=new Date,n=$e(),r=await ft(n,{fresh:e}),a=w(),l=R(a),{links:h,listens:i}=Ta(l),y=bn(),b=new Map(l.map(s=>[s.route,qn(s.owner?.session)]).filter(([,s])=>s)),S=vn(n,r,new Set(l.map(s=>s.route)),i,b),m=A(),p=Jt(t,m);return{data:{code:y.running??null,total:pt(r),start:Tn(S),sites:S,hub:On(S,h,wn(y)),home:g.realpathSync(Pe.homedir()),starting:Pn(a,t.getTime()),usage:pa({codexShare:De(W())}),pause:Ge(),upgrade:z.state(),notifications:p?p.why==="off"?{state:"off",text:"Off"}:{state:"held",text:Qt(Zt(p,t))}:{state:"on",text:"On"}},links:h,rows:n,code:y}}c(re,"sidebarState");function mt(e,t,n,r,a){let l=Buffer.isBuffer(a)?a:Buffer.from(a);const h={...r,vary:[r.vary,"accept-encoding"].filter(Boolean).join(", ")};l.length>=1024&&/\bgzip\b/.test(e.headers["accept-encoding"]||"")&&(l=vt.gzipSync(l),h["content-encoding"]="gzip"),h["content-length"]=l.length,t.writeHead(n,h),t.end(e.method==="HEAD"?void 0:l)}c(mt,"sendText");async function Oa(e){const{data:t}=await re(),n=Re.createContext({});for(const r of["ui-page-header.js","ui-sidebar.js"])Re.runInContext(g.readFileSync(k.join(pe,r),"utf8"),n);return Zn({sidebar:n.BoardUI.sidebar(t,{here:e}),sidebarData:JSON.stringify(t).replace(/</g,"\\u003c"),touchIcon:oe(180)})}c(Oa,"shellPage");const z=ca({root:We(),builders:c(async()=>(await Promise.all(R(w()).map(e=>la(e.dir)))).flat(),"builders"),run:ja,log:u,changed:c(()=>_.refresh(),"changed")});function ja(){const e=k.join(X,"kickflip-update.log");g.writeFileSync(e,`${new Date().toISOString()} kickflip update
6
+ `);const t=g.openSync(e,"a");return u("running kickflip update"),new Promise(n=>{const r=je(process.execPath,[k.join(We(),"bin","kickflip.mjs"),"update"],{detached:!0,stdio:["ignore",t,t]});g.closeSync(t),r.unref(),r.on("error",a=>n({ok:!1,error:a.message})),r.on("exit",a=>{const l=g.readFileSync(e,"utf8").trim().split(`
7
+ `).at(-1);u(`kickflip update exited ${a}: ${l}`),n(a===0?{ok:!0}:{ok:!1,error:l.replace(/^kickflip: /,"")})})})}c(ja,"runUpdate"),z.check(),setInterval(()=>z.check(),ua);const _=Xn({targets:c(()=>Object.entries(w().sites).filter(([,e])=>ln(e)&&e.port).map(([e,t])=>({route:e,port:t.port})),"targets"),watched:c(()=>R(w()).map(({route:e,dir:t})=>({route:e,dir:t})),"watched"),sidebar:c(async({fresh:e})=>(await re({fresh:e})).data,"sidebar"),log:u});function oe(e){let t="0";try{t=Math.floor(g.statSync(k.join(V,`icon-${e}.png`)).mtimeMs).toString(36)}catch{}return`/_hub/icon-${e}.png?v=${t}`}c(oe,"iconUrl");const Pa=c(()=>JSON.stringify({name:"Claude sites",short_name:"Claude",start_url:"/",scope:"/",display:"standalone",background_color:"#121514",theme_color:"#121514",icons:[{src:oe(192),sizes:"192x192",type:"image/png"},{src:oe(512),sizes:"512x512",type:"image/png"}]}),"manifestJson");async function E(e,t=16*1024){let n="";for await(const r of e)if(n+=r,n.length>t)throw Object.assign(new Error("too large"),{status:413});return n}c(E,"readBody");async function O(e,t){if(!fe(e.headers))throw Object.assign(new Error("Send the body as application/json."),{status:403});const n=await E(e,t);return n?JSON.parse(n):{}}c(O,"readJSONBody");const gt=c((e,t="")=>`<!doctype html><html lang="en" class="hub-view"><head><meta charset="utf-8"><meta name="viewport" content="width=device-width, initial-scale=1, viewport-fit=cover">
8
+ <title>${H(e)}</title><link rel="manifest" href="/manifest.webmanifest"><link rel="icon" type="image/png" href="/_hub/kickflip.png"><link rel="apple-touch-icon" href="${oe(180)}">
9
+ <meta name="apple-mobile-web-app-title" content="Claude">${t}<link rel="stylesheet" href="/_hub/colors.css?v=${Ue()}"><link rel="stylesheet" href="/_hub/hub.css"></head><body><main>`,"head"),Ra=c(e=>/\bapplication\/json\b/.test(e.headers.accept||""),"wantsJSON"),wt=c(()=>({running:!!$.running,nextAt:nt(),last:$.last}),"pollState");async function ie(e){await ot();const t=new Date,{data:n,links:r,rows:a,code:l}=await re();return{...Yt({now:t,st:le(t),poll:wt(),sidebar:n,rows:a,links:r,rules:he(),integrations:await na.rows(),code:l,host:e.headers["x-forwarded-host"]||e.headers.host||"",codexShare:De(W()),claudeModel:Ct(),codex:W(),awake:Ae(t),remembered:new Map(R(w()).map(h=>[h.route,He(h.route)]))}),prompts:Ce(null).map(Nt),remembered:He(null)}}c(ie,"hubData");const ce=c(async()=>{const e=new Date,t={until:Ae(e),unsupported:on()};return{...qt({now:e,st:le(e),rules:he(),route:await It(),linear:J(),awake:t}),closed:Un()}},"settingsData"),bt=c(e=>{const t=new Date;return en({now:t,st:le(t),poll:wt(),select:e})},"pollingData"),P=c((e,t)=>{e.writeHead(303,{location:t,"cache-control":"no-store"}),e.end()},"redirect");async function Ea(e,t,n){const r=c((s,o,d,f={})=>{t.writeHead(s,{"content-type":o,"cache-control":"no-store",...f}),t.end(d)},"send"),a=c((s,o)=>{const d=pn();return r(s,"application/json",JSON.stringify(o),d?{"x-hub-code":d}:{})},"sendJSON"),l=c(({route:s,started:o})=>(o.then(({error:d})=>{d&&u(`new board /${s} didn't start: ${d}`),_.refresh()}),_.refresh(),a(200,{route:s})),"starting"),h=Ra(e),i=n.pathname,y=/^\/_hub\/close-board\/([a-z0-9-]+)$/.exec(i);if(y&&e.method==="GET")try{return a(200,await Bn(y[1]))}catch(s){return a(s.status||400,{error:s.message})}if(["/_hub/close-board","/_hub/reopen-board"].includes(i)&&e.method==="POST"){let s;try{const o=await O(e);if(s=o.route,i==="/_hub/reopen-board"){const v=await Nn(s);return _.refresh(),a(200,v)}if(F.has(s))return a(409,{error:"That board is already closing."});F.add(s);const d=c(()=>F.delete(s),"release");t.once("finish",d),t.once("close",d),await D.running;const f=await Fn(s,o.name);return _.refresh(),a(200,f)}catch(o){return a(o.status||400,{error:o.message})}}if(i==="/_hub/restart-board"&&e.method==="POST"){let s;try{if(s=(await O(e)).route,ae.has(s))return a(409,{error:"That board is already restarting."});ae.add(s),await D.running;const o=await Gn(s,{serverUp:ba});return u(`restarted /${s}: server answers, session ${o.session} resumed`),a(200,o)}catch(o){return s&&u(`restart /${s} failed: ${o.message}`),a(o.status||400,{error:o.message})}finally{ae.delete(s),_.refresh()}}if(i==="/_hub/upgrade"&&e.method==="POST")try{return await O(e),a(200,{upgrade:await z.request()})}catch(s){return a(s.status||400,{error:s.message})}if(i==="/_hub/wake"&&e.method==="POST")try{const{route:s}=await O(e),o=R(w()).find(d=>d.route===s);return o?(ye.add(s),await D.running,rt(),await D.running,_e.clear(),_.changed(s),a(200,{route:s,listen:Ie(o)})):a(404,{error:"That board is not open."})}catch(s){return a(s.status||400,{error:s.message})}if(i==="/_hub/repos"&&e.method==="GET")try{return a(200,await L.list(w(),{fresh:n.searchParams.get("fresh")==="1"}))}catch(s){return a(s.status||502,{error:s.message})}if(i==="/_hub/branches"&&e.method==="GET")try{return a(200,await L.branches(n.searchParams.get("github")??n.searchParams.get("remote")))}catch(s){return a(s.status||502,{error:s.message})}if(i==="/_hub/repo"&&e.method==="GET")return En(n,{repos:L,manifest:w(),sendJSON:a});const b=/^\/_hub\/board-repo\/([a-z0-9-]+)$/.exec(i);if(b&&["GET","POST"].includes(e.method))try{const s=e.method==="GET"?Dn(b[1]):await Ln(b[1],await O(e),{repos:L});return e.method==="POST"&&_.refresh(),a(200,s)}catch(s){return a(s.status||400,{error:s.message})}if(i==="/_hub/new-board"&&e.method==="POST"){if(!fe(e.headers))return r(403,"text/plain; charset=utf-8","Send name, github, branch and purpose as application/json.");let s;try{s=await O(e)}catch{return r(400,"text/plain; charset=utf-8","Send name, github, branch and purpose as JSON.")}try{return l(await An(s,{repos:L,changed:c(()=>_.refresh(),"changed")}))}catch(o){if(o.status)return r(o.status,"text/plain; charset=utf-8",o.message);throw o}}const S=/^\/_hub\/new-board\/([a-z0-9-]+)\/retry$/.exec(i);if(S&&e.method==="POST"){if(!fe(e.headers))return r(403,"text/plain; charset=utf-8","Send the retry as application/json.");try{return l(await In(S[1],{repos:L,changed:c(()=>_.refresh(),"changed")}))}catch(s){if(s.status)return r(s.status,"text/plain; charset=utf-8",s.message);throw s}}const m=/^\/_hub\/new-board\/([a-z0-9-]+)$/.exec(i);if(m&&e.method==="DELETE")try{const s=await Mn(m[1]);return _.refresh(),a(200,s)}catch(s){if(s.status)return r(s.status,"text/plain; charset=utf-8",s.message);throw s}if(i==="/_hub/build"&&e.method==="POST"){if(q)return a(503,{error:"The hub is stopping. Try again after it restarts."});const s=["127.0.0.1","::1","::ffff:127.0.0.1"],o=(e.headers.host||"").replace(/:\d+$/,"");if(!s.includes(e.socket.remoteAddress)||!["127.0.0.1","localhost","[::1]"].includes(o)||e.headers.origin||Object.keys(e.headers).some(f=>f==="forwarded"||f.startsWith("x-forwarded-")))return a(403,{error:"Builds can only be started from the host over loopback."});let d;try{d=await O(e)}catch(f){return a(f.status||400,{error:"Send the board route and card key as JSON."})}try{return a(202,await sa(d,{manifest:w(),log:u}))}catch(f){if(f instanceof aa)return a(f.status,{error:f.message});throw f}}if(i==="/sw.js")return r(200,"text/javascript; charset=utf-8",g.readFileSync(k.join(V,"sw.js")),{"service-worker-allowed":"/"});if(i==="/manifest.webmanifest")return r(200,"application/manifest+json",Pa());const p=/^\/_hub\/icon-(180|192|512)\.png$/.exec(i);if(p)try{return r(200,"image/png",g.readFileSync(k.join(V,`icon-${p[1]}.png`)),{"cache-control":"max-age=86400"})}catch{t.writeHead(404).end();return}if(i==="/_hub/colors.css"){const s=n.searchParams.get("v")===Ue()?"public, max-age=31536000, immutable":"no-cache";try{return r(200,"text/css; charset=utf-8",g.readFileSync(fa),{"cache-control":s})}catch{t.writeHead(404).end();return}}if(i==="/_hub/kickflip.png")try{return r(200,"image/png",g.readFileSync(k.join(pe,"kickflip.png")),{"cache-control":"max-age=86400"})}catch{t.writeHead(404).end();return}if(i==="/_hub/hub.css")return r(200,"text/css; charset=utf-8",g.readFileSync(k.join(pe,"hub.css")));if(i==="/_hub/sidebar.json")return a(200,(await re({fresh:n.searchParams.get("fresh")==="1"})).data);if(i==="/_hub/stream"&&e.method==="GET"){_.handle(e,t);return}if(i.startsWith("/_hub/app/")){const s=Wn(i.slice(10));if(!s){t.writeHead(404).end();return}const o=k.extname(s),d={".js":"text/javascript; charset=utf-8",".css":"text/css; charset=utf-8",".png":"image/png"}[o],f=n.searchParams.get("v")===Vn(s)?"public, max-age=31536000, immutable":"no-cache";return o===".png"?r(200,d,g.readFileSync(s),{"cache-control":f}):mt(e,t,200,{"content-type":d,"cache-control":f},g.readFileSync(s))}if(i==="/_hub/api/hub")return a(200,await ie(e));if(i==="/_hub/api/settings")return a(200,await ce());if(i==="/_hub/api/polling")return a(200,bt(n.searchParams.get("w")));if(i==="/_hub/push/key")return a(200,{key:Tt()});if(i==="/_hub/push/subscribe"&&e.method==="POST"){const s=e.headers["x-forwarded-host"]||e.headers.host;return a(200,{count:Ot(await O(e),`https://${s}/`,e.headers["user-agent"])})}if(i==="/_hub/push/unsubscribe"&&e.method==="POST")return a(200,{count:jt((await O(e))?.endpoint)});if(i==="/_hub/push/test"&&e.method==="POST"){const s=await Pt({title:"Claude",body:"Test notification from the hub.",url:"/",tag:"hub-test"});return a(200,{sent:s.filter(o=>o.status>=200&&o.status<300).length,results:s})}if(i==="/_hub/settings/notifications"&&e.method==="POST"){const s=new URLSearchParams(await E(e)),o=s.get("set"),d=o==="on"?{on:s.get("value")==="on"}:o==="weekends"?{weekends_off:s.get("value")==="on"}:s.has("snooze")?{snooze:s.get("snooze")}:null,f="Pass set=on or set=weekends with value=on, or snooze.";if(!d)return h?a(400,{error:f}):r(400,"text/plain; charset=utf-8",f);let v;try{v=await Dt(d)}catch(K){if(K.status!==400)throw K;return h?a(400,{error:nn}):P(t,"/_hub/?refused=snooze#notifications")}return v.dropped&&u(`notifications off: dropped ${v.dropped} queued push${v.dropped===1?"":"es"}`),h?a(200,await ce()):P(t,"/_hub/#notifications")}if(i==="/_hub/settings/keep-awake"&&e.method==="POST"){const s=new URLSearchParams(await E(e));try{rn(s.get("keep_awake"))}catch(o){if(o.status===409)return h?a(409,{error:o.message}):r(409,"text/plain; charset=utf-8",o.message);if(o.status!==400)throw o;return h?a(400,{error:an}):P(t,"/_hub/?refused=keep-awake#keep-awake")}return Q.apply(),h?a(200,await ce()):P(t,"/_hub/#keep-awake")}if(i==="/_hub/settings/codex"&&e.method==="POST"){const s=new URLSearchParams(await E(e));try{Lt(s.get("value"),W({fresh:!0}))}catch(o){if(o.status!==400&&o.status!==409)throw o;return h?a(o.status,{error:o.message}):r(o.status,"text/plain; charset=utf-8",o.message)}return h?a(200,await ie(e)):P(t,"/_hub/")}if(i==="/_hub/settings/claude-model"&&e.method==="POST"){const s=new URLSearchParams(await E(e));try{Ht(s.get("value"))}catch(o){if(o.status!==400)throw o;return h?a(400,{error:o.message}):r(400,"text/plain; charset=utf-8",o.message)}return h?a(200,await ie(e)):P(t,"/_hub/")}if(["/_hub/settings/pause-at","/_hub/settings/keep-going"].includes(i)&&e.method==="POST"){const s=new URLSearchParams(await E(e));try{i.endsWith("/pause-at")?ea(s.get("value")):ta(s.get("value")),await ke()}catch(o){if(o.status!==400&&o.status!==409)throw o;return h?a(o.status,{error:o.message}):r(o.status,"text/plain; charset=utf-8",o.message)}return h?a(200,await ie(e)):P(t,"/_hub/")}if(i==="/_hub/settings/linear"&&e.method==="POST"){const s=new URLSearchParams(await E(e,4096));try{s.has("remove")?(da(),u("linear: key removed")):u(`linear: key ${(await ha(s.get("key"))).refused?"refused by Linear":"saved"}`)}catch(o){if(!(o instanceof me))throw o;return h?a(o.status,{error:o.message}):r(o.status,"text/plain; charset=utf-8",o.message)}return h?a(200,await ce()):P(t,"/_hub/#linear")}if(i==="/_hub/api/linear"&&e.method==="GET"){const s=J();if(!s.set||s.refused)return a(200,s);const o=Je();try{const d=await o.teams();return a(200,{...J(),teams:d.map(({key:f,name:v})=>({key:f,name:v})),projects:d.flatMap(f=>f.projects.map(v=>({id:v.id,name:v.name,team:f.key})))})}catch(d){if(!(d instanceof me))throw d;return a(200,{...J(),error:d.message})}}if(i==="/_hub/linear/projects"&&e.method==="POST"){const s=Je();if(!s)return a(400,{error:"Add a Linear key on Hub first."});let o;try{o=await O(e,4*1024)}catch(f){return a(f.status||400,{error:"Send the team and project name as JSON."})}const d=typeof o.name=="string"?o.name.trim():"";if(!d||d.length>80||typeof o.team!="string"||!/^[A-Z][A-Z0-9]*$/.test(o.team))return a(400,{error:"Name the project and its team."});try{return a(200,await s.createProject(o.team,d))}catch(f){if(!(f instanceof me))throw f;return a(f.status,{error:f.message})}}if(i==="/_hub/settings/polling"&&e.method==="POST"){const s=new URLSearchParams(await E(e)),o=s.get("op")||"",d=Xt(A(),{op:o,w:s.get("w")||"",value:(s.get("value")||"").trim()});try{if(d.error)throw Object.assign(new Error(d.error),{status:400});Rt(d.input)}catch(f){if(f.status!==400)throw f;return h?a(400,{error:f.message}):r(400,"text/plain; charset=utf-8",f.message)}return h?a(200,bt(d.select)):P(t,`/_hub/?w=${encodeURIComponent(d.select)}#polling`)}if(i==="/_hub/settings/rules"&&e.method==="POST"){let s,o;try{s=await O(e,64*1024),o=kn(he(),s.rules,s.base??[]),Fe(o.changes)&&$n(o.rules)}catch(f){if(!(f instanceof Sn)&&!(f instanceof SyntaxError))throw f;return a(400,{error:f.message})}if(!Fe(o.changes))return a(200,{rules:o.rules,changed:!1,events:[],failed:[]});const d=await Me(R(w()),"rules",{scope:"hub",...o.changes});return u(`hub rules saved: rules \u2192 ${d.events.map(f=>f.route).join(", ")||"no board"}${d.failed.length?`; ${d.failed.join("; ")}`:""}`),a(200,{rules:o.rules,changed:!0,...d})}if(i==="/_hub/push/flush"&&e.method==="POST"){const s=await Ee();return s&&u(`push queue: ${s.ok?"sent":"could not send"} ${s.count} queued (from the settings page)`),h?a(200,tn(s)):P(t,`/_hub/settings/polling?flushed=${s?s.ok?s.count:"failed":0}`)}if(i==="/_hub/poll"&&e.method==="POST"){const s=await O(e);return a(200,await at({flush:s.flush!==!1}))}if(i==="/_hub/prompts"&&e.method==="POST"){let s;try{s=await O(e,8*1024*1024)}catch(f){if(f.status)throw f;return a(400,{error:"Send the hook input as JSON."})}if(!s?.tool_name||s.hook_event_name&&s.hook_event_name!=="PermissionRequest")return a(400,{error:"Send a PermissionRequest hook input."});const{answer:o,rec:d}=await I(f=>{const v=Se(),K=v.boards.find(ve=>ve.owner?.session&&ve.owner.session===s.session_id)||null;de(f,v)&&_.refresh();const xe=zt(f,s);if(xe)return{answer:xe};const ue=Ft(s,{board:K,live:v.running.get(s.session_id)});return f.prompts[ue.id]=ue,{rec:ue}});return o?a(200,{state:"answered",answer:o}):(N(d.route),u(`prompt ${d.id}: ${d.route?`/${d.route}`:d.cwd} ${d.tool}${d.action?` (${d.action})`:""}`),At(Ut(d),{kind:"prompt",immediate:!0}).catch(f=>u(`prompt ${d.id}: push failed: ${f.message}`)),a(200,{id:d.id,deadline:d.deadline}))}const x=/^\/_hub\/prompts\/([\w-]+)(\/wait|\/answer)?$/.exec(i);return x?Da(e,t,{id:x[1],op:x[2]?.slice(1)||null,url:n,sendJSON:a}):!1}c(Ea,"hubRoute");async function Da(e,t,{id:n,op:r,url:a,sendJSON:l}){if(r==="wait"&&e.method==="GET"){const h=c(()=>I(m=>{const p=j(m.prompts,n);return p?.answer?delete m.prompts[n]:p&&(p.seen=new Date().toISOString()),p}),"seen"),i=c(m=>(N(m.route),l(200,{state:"answered",answer:m.answer})),"take"),y=await h();if(!y)return l(404,{state:"gone"});if(y.answer)return i(y);const b=Math.max(0,Math.min(Number(a.searchParams.get("ms"))||Le,Le));if(await new Promise(m=>{const p=U.get(n)||new Set;U.set(n,p);const x=c(()=>{clearTimeout(s),p.delete(x),!p.size&&U.get(n)===p&&U.delete(n),m()},"done"),s=setTimeout(x,b);p.add(x),t.on("close",x)}),t.writableEnded||t.destroyed)return;const S=await h();return S?S.answer?i(S):l(200,{state:"pending"}):l(404,{state:"gone"})}if(r==="answer"&&e.method==="POST"){const h=await O(e),i=await I(S=>{const m=j(S.prompts,n);if(!m)return{status:404,error:"That prompt has expired."};if(m.answer)return{status:409,error:"That prompt was already answered."};const{answer:p,error:x}=Mt(m,h);if(x)return{status:400,error:x};m.answer=p;const s=Se();return de(S,s),Gt(S,m,s),{rec:m,answer:p}});if(i.error)return l(i.status,{error:i.error});const{rec:y,answer:b}=i;u(`prompt ${n}: ${b.choice}`);for(const S of[...U.get(n)||[]])S();return N(y.route),l(200,{ok:!0})}if(r===null&&e.method==="DELETE"){const h=await I(i=>{const y=j(i.prompts,n);return delete i.prompts[n],y});return h&&(N(h.route),u(`prompt ${n}: its hook gave up`)),l(200,{ok:!0})}return!1}c(Da,"promptRoute");const La=c(({icon:e="",title:t,sub:n=""})=>`<div class="row">${e}<span class="body"><span class="text"><span class="title">${t}</span>${n?`<span class="sub">${n}</span>`:""}</span></span></div>`,"row");function yt(e,t){const n=T.get(e)||{},r=n.exitCode!==void 0&&n.exitCode!==null?` (last exit: ${n.exitCode})`:"",a=M(t),[l,h]=a==="port"?["down",`down \xB7 nothing answers on port ${t.port}`]:a==="files"?["down","down \xB7 its folder is missing"]:n.child?["wait","started \xB7 waiting for it to answer"]:["down",`down \xB7 restarting${r}`],i=a==="cmd"?"The hub runs every site and restarts one that stops.":a==="port"?"Its session runs this server, so it comes back when that session starts it again.":"The hub serves this page from a folder that is no longer there.";return`${gt(`${t.title||e} is down`,'<meta http-equiv="refresh" content="5">')}
10
+ <a class="back" href="/_hub/">\u2039 Hub</a>
11
+ <h1>${H(t.title||e)} isn't answering</h1>
12
+ <p class="lede">${H(i)}</p>
13
+ <h2 class="label">Site</h2>
14
+ <div class="group">${La({icon:_n(e,t),title:H(t.title||e),sub:`<span class="st ${l}">${H(h)}</span>`})}</div>
15
+ <p class="note">This page retries every 5 seconds.</p>
16
+ </main></body></html>`}c(yt,"downPage");const St=c(e=>new URL(e.url.replace(/^\/+/,"/"),"http://hub"),"requestURL"),kt=Te.createServer((e,t)=>{const n=St(e);if(e.method!=="GET"&&e.method!=="HEAD"&&Kn(e.headers)){t.writeHead(403,{"content-type":"text/plain; charset=utf-8","cache-control":"no-store"}),t.end("Requests must come from the app\u2019s own pages.");return}const r=new Set(Object.keys(w().sites));if(Ne({method:e.method,pathname:n.pathname,search:n.search,dest:Jn(e.headers),sites:r})==="shell"){const m=n.pathname==="/"?"":n.pathname.startsWith("/_hub/")?n.pathname.slice(1):decodeURIComponent(n.pathname.split("/")[1]);Oa(m).then(p=>mt(e,t,200,{"content-type":"text/html; charset=utf-8","cache-control":"no-cache",vary:"sec-fetch-dest","x-app-shell":"1"},p)).catch(p=>{t.headersSent||t.writeHead(500,{"content-type":"text/plain"}),t.end(String(p.message||p))});return}if(n.pathname==="/_hub"){t.writeHead(302,{location:"/_hub/"+n.search}).end();return}if(n.pathname==="/_hub/status.json"){t.writeHead(200,{"content-type":"application/json","cache-control":"no-store"}),t.end(JSON.stringify({hub:{port:ge,pid:process.pid},sites:$e()},null,2));return}if(n.pathname==="/_hub/needs.json"){const m=$e();ft(m,{fresh:n.searchParams.get("fresh")==="1"}).then(p=>{t.writeHead(200,{"content-type":"application/json","cache-control":"no-store"}),t.end(JSON.stringify({total:pt(p),sites:Object.fromEntries(m.map(x=>[x.route,p.get(x.route)??null]))}))}).catch(p=>{t.headersSent||t.writeHead(500,{"content-type":"text/plain"}),t.end(String(p.message||p))});return}if(n.pathname==="/favicon.ico"){t.writeHead(301,{location:"/_hub/kickflip.png"}).end();return}if(n.pathname==="/sw.js"||n.pathname==="/manifest.webmanifest"||n.pathname.startsWith("/_hub/")){Ea(e,t,n).then(m=>{m===!1&&t.writeHead(404).end()}).catch(m=>{t.headersSent||t.writeHead(m.status||500,{"content-type":"text/plain"}),t.end(String(m.message||m))});return}const a=n.pathname.split("/")[1],l=j(w().sites,a);if(!l){t.writeHead(404,{"content-type":"text/html; charset=utf-8"}),t.end(`${gt("No Site")}<a class="back" href="/_hub/">\u2039 Hub</a><h1>No site at /${H(a)}</h1></main></body></html>`);return}if(n.pathname==="/"+a){t.writeHead(302,{location:"/"+a+"/"+n.search}).end();return}const h="/"+a,i=e.url.slice(h.length)||"/",y=Ne({pathname:n.pathname,dest:"document",sites:r})==="shell";if(M(l)==="files"){if(!dt(l.dir)){t.writeHead(502,{"content-type":"text/html; charset=utf-8","cache-control":"no-store"}),t.end(yt(a,l));return}ia(e,t,{dir:l.dir,rest:i,prefix:h,headers:y?{vary:"sec-fetch-dest"}:{}}).catch(m=>{t.headersSent||t.writeHead(500,{"content-type":"text/plain"}),t.end(String(m.message||m))});return}const b={...e.headers,"x-forwarded-prefix":h,"x-forwarded-host":e.headers["x-forwarded-host"]||e.headers.host,"x-forwarded-proto":e.headers["x-forwarded-proto"]||"https"},S=Te.request({host:"127.0.0.1",port:l.port,method:e.method,path:i,headers:b},m=>{const p={...m.headers};y&&(p.vary=[p.vary,"sec-fetch-dest"].filter(Boolean).join(", ")),typeof p.location=="string"&&p.location.startsWith("/")&&!p.location.startsWith("//")&&!p.location.startsWith(h+"/")&&p.location!==h&&(p.location=h+p.location),t.writeHead(m.statusCode,p),m.pipe(t),m.on("close",()=>{m.complete||t.destroy()})});t.on("close",()=>{t.writableFinished||S.destroy()}),S.on("error",()=>{if(t.headersSent){t.destroy();return}t.writeHead(502,{"content-type":"text/html; charset=utf-8","cache-control":"no-store"}),t.end(yt(a,l))}),e.pipe(S)});kt.on("upgrade",(e,t,n)=>{const r=St(e).pathname.split("/")[1],a=j(w().sites,r);if(!a?.port){t.destroy();return}const l=`/${r}`,h=e.url.slice(l.length)||"/",i=Oe.connect(a.port,"127.0.0.1",()=>{const y=[`${e.method} ${h} HTTP/${e.httpVersion}`];for(let b=0;b<e.rawHeaders.length;b+=2)e.rawHeaders[b].toLowerCase()!=="x-forwarded-prefix"&&y.push(`${e.rawHeaders[b]}: ${e.rawHeaders[b+1]}`);y.push(`X-Forwarded-Prefix: ${l}`),i.write(y.join(`\r
17
+ `)+`\r
18
+ \r
19
+ `),n&&n.length&&i.write(n),i.pipe(t),t.pipe(i)});i.on("error",()=>t.destroy()),t.on("error",()=>i.destroy()),t.on("close",()=>i.destroy()),i.on("close",()=>t.destroy())}),kt.listen(ge,"127.0.0.1",()=>u(`hub on http://127.0.0.1:${ge}/ (manifest ${Ve})`));
Binary file
Binary file
Binary file
package/hub/icons.mjs ADDED
@@ -0,0 +1 @@
1
+ var p=Object.defineProperty;var h=(t,a)=>p(t,"name",{value:a,configurable:!0});const M=/^#([0-9a-f]{3}|[0-9a-f]{6})$/i,o={phone:'<rect x="6" y="2" width="12" height="20" rx="2.5"/><path d="M11 18h2"/>',audit:'<circle cx="11" cy="11" r="7"/><path d="m20 20-3.5-3.5"/><path d="m8.5 11 1.8 1.8 3.4-3.6"/>',magnifier:'<circle cx="11" cy="11" r="7"/><path d="m20 20-3.5-3.5"/>',clipboard:'<rect x="8" y="2" width="8" height="4" rx="1"/><path d="M16 4h2a2 2 0 0 1 2 2v14a2 2 0 0 1-2 2H6a2 2 0 0 1-2-2V6a2 2 0 0 1 2-2h2"/>',book:'<path d="M2 5h6a4 4 0 0 1 4 4v11a3 3 0 0 0-3-3H2z"/><path d="M22 5h-6a4 4 0 0 0-4 4v11a3 3 0 0 1 3-3h7z"/>',cap:'<path d="M21.42 10.92a1 1 0 0 0-.02-1.84L12.83 5.18a2 2 0 0 0-1.66 0L2.6 9.08a1 1 0 0 0 0 1.83l8.57 3.91a2 2 0 0 0 1.66 0z"/><path d="M22 10v6"/><path d="M6 12.5V16a6 3 0 0 0 12 0v-3.5"/>',chart:'<path d="M3 3v16a2 2 0 0 0 2 2h16"/><path d="M18 17V9"/><path d="M13 17V5"/><path d="M8 17v-3"/>',code:'<path d="m16 18 6-6-6-6"/><path d="m8 6-6 6 6 6"/>',terminal:'<path d="m4 17 6-6-6-6"/><path d="M12 19h8"/>',chat:'<path d="M21 15a2 2 0 0 1-2 2H7l-4 4V5a2 2 0 0 1 2-2h14a2 2 0 0 1 2 2z"/>',calendar:'<rect x="3" y="4" width="18" height="18" rx="2"/><path d="M16 2v4M8 2v4M3 10h18"/>',map:'<path d="M14.1 5.55a2 2 0 0 0 1.8 0l3.65-1.83A1 1 0 0 1 21 4.62v12.76a1 1 0 0 1-.55.9l-4.56 2.27a2 2 0 0 1-1.78 0l-4.22-2.1a2 2 0 0 0-1.78 0l-3.66 1.83A1 1 0 0 1 3 19.38V6.62a1 1 0 0 1 .55-.9l4.56-2.27a2 2 0 0 1 1.78 0z"/><path d="M15 5.76v15"/><path d="M9 3.24v15"/>',wrench:'<path d="M14.7 6.3a1 1 0 0 0 0 1.4l1.6 1.6a1 1 0 0 0 1.4 0l3.77-3.77a6 6 0 0 1-7.94 7.94l-6.91 6.91a2.12 2.12 0 0 1-3-3l6.91-6.91a6 6 0 0 1 7.94-7.94l-3.76 3.76z"/>',server:'<rect x="3" y="3" width="18" height="7" rx="2"/><rect x="3" y="14" width="18" height="7" rx="2"/><path d="M7 6.5h.01M7 17.5h.01"/>',car:'<path d="M19 17h2a1 1 0 0 0 1-1v-3a2 2 0 0 0-1.5-1.9C18.7 10.6 16 10 16 10l-2.2-2.3A2.5 2.5 0 0 0 12 7H5a1.6 1.6 0 0 0-1.4.9l-1.4 2.9A3.7 3.7 0 0 0 2 12v4a1 1 0 0 0 1 1h2"/><circle cx="7" cy="17" r="2"/><path d="M9 17h6"/><circle cx="17" cy="17" r="2"/>',flask:'<path d="M9 3h6"/><path d="M10 3v6.5L4.5 19a1.5 1.5 0 0 0 1.3 2h12.4a1.5 1.5 0 0 0 1.3-2L14 9.5V3"/><path d="M7 15h10"/>',bell:'<path d="M6 8a6 6 0 0 1 12 0c0 7 3 9 3 9H3s3-2 3-9"/><path d="M10.3 21a1.94 1.94 0 0 0 3.4 0"/>',list:'<path d="M9 6h12M9 12h12M9 18h12"/><path d="M4 6h.01M4 12h.01M4 18h.01"/>',globe:'<circle cx="12" cy="12" r="10"/><path d="M2 12h20"/><path d="M12 2a15.3 15.3 0 0 1 4 10 15.3 15.3 0 0 1-4 10 15.3 15.3 0 0 1-4-10 15.3 15.3 0 0 1 4-10z"/>'},y=Object.keys(o),v=[["phone",["ios","iphone","phone","mobile","android"]],["audit",["audit","review","inspect"]],["magnifier",["search","research","find","explore","investigate"]],["book",["learn","course","lesson","lessons","docs","book","guide","teach","study"]],["cap",["school","class","training","tutor"]],["chart",["metrics","dashboard","stats","analytics","chart","report","reports"]],["chat",["chat","ask","questions","qa","talk","voice"]],["calendar",["calendar","schedule","agenda"]],["map",["map","maps","routing","trip","travel"]],["flask",["prototype","prototypes","experiment","lab"]],["terminal",["cli","shell","terminal","script","scripts"]],["code",["code","pr","prs","build","dev","wave","waves"]],["server",["backend","server","api","db","database","sync"]],["car",["car","cars","vehicle","vehicles","inventory"]],["wrench",["tools","tool","fix","ops","setup"]],["bell",["alerts","notifications","notify"]],["list",["todo","todos","tasks","checklist","board"]]];function x(t,a={}){if(Object.hasOwn(o,a.icon||""))return a.icon;const c=new Set(`${t} ${a.title||""}`.toLowerCase().split(/[^a-z0-9]+/));return v.find(([,r])=>r.some(e=>c.has(e)))?.[0]||null}h(x,"iconKey");const s=["#2563eb","#7c3aed","#db2777","#ea580c","#059669","#0891b2","#4f46e5","#b45309"],d=h(t=>String(t??"").replace(/[&<>"']/g,a=>({"&":"&amp;","<":"&lt;",">":"&gt;",'"':"&quot;","'":"&#39;"})[a]),"esc");function b(t,a={}){const c=[...t].reduce((l,n)=>l*31+n.codePointAt(0)>>>0,0),r=M.test(a.color||"")?a.color:s[c%s.length],e=x(t,a),i=e?`<svg width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round">${o[e]}</svg>`:d([...a.title||t][0].toUpperCase());return`<span class="icon${e?"":" letter"}" style="--c:${d(r)}" aria-hidden="true"${e?` data-icon="${e}"`:""}>${i}</span>`}h(b,"siteIcon");export{o as ICONS,y as ICON_KEYS,x as iconKey,b as siteIcon};
package/hub/linear.mjs ADDED
@@ -0,0 +1,2 @@
1
+ var j=Object.defineProperty;var a=(s,e)=>j(s,"name",{value:e,configurable:!0});import f from"node:fs";import $ from"node:path";import C from"node:os";class l extends Error{static{a(this,"LinearError")}constructor(e,r=502){super(e),this.status=r}}const k=a((s=C.homedir())=>$.join(s,".claude","kickflip","linear.json"),"keyFile"),_=a(()=>process.env.KICKFLIP_LINEAR_URL||"https://api.linear.app/graphql","endpoint");function g(s){try{const e=JSON.parse(f.readFileSync(k(s),"utf8"));return typeof e?.key=="string"&&e.key?e:null}catch{return null}}a(g,"readLinearKey");function v(s){const e=g(s);return e?{set:!0,...e.refused_at?{refused:!0}:{},...e.workspace?{workspace:e.workspace,url_key:e.url_key}:{}}:{set:!1}}a(v,"linearStatus");function S(s){const e=k();f.mkdirSync($.dirname(e),{recursive:!0});const r=`${e}.${process.pid}.tmp`;f.writeFileSync(r,JSON.stringify(s,null,2)+`
2
+ `,{mode:384}),f.chmodSync(r,384),f.renameSync(r,e)}a(S,"writeKeyFile");function I(s,{fetch:e=globalThis.fetch,checked:r=a(()=>{},"checked")}={}){async function u(n,t={}){let i;try{i=await e(_(),{method:"POST",headers:{"content-type":"application/json",authorization:s},body:JSON.stringify({query:n,variables:t}),signal:AbortSignal.timeout(15e3)})}catch(c){throw new l(`Could not reach Linear: ${c.message}`)}const o=await i.json().catch(()=>null);if(i.status===401||o?.errors?.some(c=>c.extensions?.type==="authentication error"))throw r(!1),new l("Linear refused the key.",400);if(o?.errors?.length){const c=o.errors[0];throw Object.assign(new l(`Linear: ${c.message}`),{notFound:c.extensions?.type==="not found"||/not found/i.test(c.message)})}if(!i.ok||!o?.data)throw new l(`Linear answered ${i.status}.`);return r(!0),o.data}a(u,"call");const p="id key name states { nodes { id name type position } } projects(first: 100) { nodes { id name } }";async function d(n){return(await u(`query($key: String!) { teams(filter: { key: { eq: $key } }) { nodes { ${p} } } }`,{key:n})).teams.nodes[0]||(()=>{throw new l(`Linear has no team ${n}.`,400)})()}a(d,"team");const m=a((n,t)=>n.projects.nodes.find(i=>i.id===t)||n.projects.nodes.find(i=>i.name.toLowerCase()===String(t).toLowerCase())||(()=>{throw new l(`Linear's ${n.key} team has no project ${t}.`,400)})(),"projectOf"),y={async viewer(){const n=await u("query { viewer { id name } organization { name urlKey } }");return{viewer:{id:n.viewer.id,name:n.viewer.name},workspace:n.organization.name,url_key:n.organization.urlKey}},async teams(){const n=await u("query { teams(first: 100) { nodes { id key name } } }");return Promise.all(n.teams.nodes.map(async t=>{const i=await u("query($id: String!) { team(id: $id) { projects(first: 100) { nodes { id name } } } }",{id:t.id});return{id:t.id,key:t.key,name:t.name,projects:i.team.projects.nodes.map(o=>({id:o.id,name:o.name}))}}))},async createProject(n,t){const i=await d(n),o=await u("mutation($input: ProjectCreateInput!) { projectCreate(input: $input) { success project { id name } } }",{input:{name:t,teamIds:[i.id]}});if(!o.projectCreate.success)throw new l("Linear did not create the project.");return{...o.projectCreate.project,team:i.key}},async createIssue({team:n,project:t,title:i,description:o}){const c=await d(n),w={teamId:c.id,projectId:m(c,t).id,title:i,...o?{description:o}:{}},h=await u("mutation($input: IssueCreateInput!) { issueCreate(input: $input) { success issue { identifier url } } }",{input:w});if(!h.issueCreate.success)throw new l("Linear did not create the issue.");return h.issueCreate.issue},async issue(n){try{const t=await u("query($id: String!) { issue(id: $id) { identifier team { key } assignee { id } state { id name type } } }",{id:n});return t.issue?{identifier:t.issue.identifier,team:t.issue.team.key,assignee:t.issue.assignee?.id??null,state:t.issue.state}:null}catch(t){if(t.notFound)return null;throw t}},async advanceIssue(n,t,{owner:i,created:o}={}){const c=await y.issue(n);if(!c||c.assignee!==i&&!o||x(c.state)>={started:1,review:2,done:3}[t])return null;const w=L((await d(c.team)).states.nodes,t);if(!w)return null;if(!(await u("mutation($id: String!, $stateId: String!) { issueUpdate(id: $id, input: { stateId: $stateId }) { success } }",{id:n,stateId:w.id})).issueUpdate.success)throw new l("Linear did not move the issue.");return w.name},async moveIssue(n,t){const i=await y.issue(n);if(!i)throw new l(`Linear has no issue ${n}.`,400);if(["completed","canceled"].includes(i.state.type))return null;const o=L((await d(i.team)).states.nodes,t);if(!o||o.id===i.state.id)return null;if(!(await u("mutation($id: String!, $stateId: String!) { issueUpdate(id: $id, input: { stateId: $stateId }) { success } }",{id:n,stateId:o.id})).issueUpdate.success)throw new l("Linear did not move the issue.");return o.name}};return y}a(I,"linearClient");function x(s){return["completed","canceled"].includes(s.type)?3:s.type==="started"?/review/i.test(s.name)?2:1:0}a(x,"stageOf");function L(s,e){const r=[...s].sort((d,m)=>d.position-m.position),u=a(d=>r.filter(m=>m.type===d),"of"),p=a(d=>/review/i.test(d.name),"review");return e==="started"?u("started").find(d=>/^in progress$/i.test(d.name))||u("started").find(d=>!p(d))||null:e==="review"?u("started").find(p)||null:e==="done"&&(u("completed").find(d=>/^done$/i.test(d.name))||u("completed")[0])||null}a(L,"stateFor");async function U(s,{fetch:e}={}){const r=typeof s=="string"?s.trim():"";if(!r||/\s/.test(r)||r.length>200)throw new l("Paste a Linear personal API key.",400);const u=new Date().toISOString();let p=!1;const d=await I(r,{fetch:e,checked:a(m=>{p=!m},"checked")}).viewer().catch(m=>{if(!p)throw m;return null});return S(d?{key:r,...d,saved_at:u}:{key:r,saved_at:u,refused_at:u}),v()}a(U,"saveLinearKey");function F(s,e){const r=g();if(r?.key!==s||!!r.refused_at==!e)return;const{refused_at:u,...p}=r;S(e?p:{...p,refused_at:new Date().toISOString()})}a(F,"markChecked");function b(){return f.rmSync(k(),{force:!0}),v()}a(b,"removeLinearKey");function z({fetch:s}={}){const e=g();return e?{...I(e.key,{fetch:s,checked:a(r=>F(e.key,r),"checked")}),saved:e}:null}a(z,"savedClient");export{l as LinearError,I as linearClient,v as linearStatus,g as readLinearKey,b as removeLinearKey,U as saveLinearKey,z as savedClient,x as stageOf,L as stateFor};
package/hub/live.mjs ADDED
@@ -0,0 +1,2 @@
1
+ var m=Object.defineProperty;var s=(t,n)=>m(t,"name",{value:n,configurable:!0});import p from"node:fs";import c from"node:path";import d from"node:os";import{execFileSync as l}from"node:child_process";const a=c.join(d.homedir(),".claude","sessions"),f=2e3,o=s(t=>Date.parse(`${String(t).trim().replace(/\s+/g," ")} UTC`),"utc");function S(t,n){return t.filter(e=>{if(!n.has(e.pid))return!1;if(typeof e.procStart!="string")return!0;const r=o(e.procStart);return Number.isNaN(r)||Math.abs(r-n.get(e.pid))<=f})}s(S,"liveOf");function g(t=a){let n=[];try{n=p.readdirSync(t).filter(e=>e.endsWith(".json"))}catch{}return n.flatMap(e=>{try{const r=JSON.parse(p.readFileSync(c.join(t,e),"utf8"));return Number.isInteger(r?.pid)&&typeof r.sessionId=="string"?[r]:[]}catch{return[]}})}s(g,"sessionRecords");function u(t){const n=new Map;if(!t.length)return n;let e="";try{e=l("ps",["-o","pid=,lstart=","-p",t.join(",")],{encoding:"utf8",timeout:3e3,env:{...process.env,TZ:"UTC"}})}catch(r){e=r.stdout||""}for(const r of e.split(`
2
+ `)){const i=r.trim().match(/^(\d+)\s+(.+)$/);i&&n.set(Number(i[1]),o(i[2]))}return n}s(u,"processStarts");function I(t,n=u){if(!Number.isInteger(t?.pid)||t.pid<=0||!t.sessionId||typeof t.procStart!="string")return!1;const e=o(t.procStart);if(!Number.isFinite(e))return!1;const r=n([t.pid]).get(t.pid);return Number.isFinite(r)&&Math.abs(e-r)<=f}s(I,"verifiedProcess");function v({dir:t=a,starts:n=u}={}){const e=g(t);return new Map(S(e,n([...new Set(e.map(r=>r.pid))])).map(r=>[r.sessionId,r]))}s(v,"liveSessions");export{a as SESSIONS_DIR,S as liveOf,v as liveSessions,u as processStarts,g as sessionRecords,I as verifiedProcess};
package/hub/locks.mjs ADDED
@@ -0,0 +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};
@@ -0,0 +1,2 @@
1
+ var B=Object.defineProperty;var o=(e,t)=>B(e,"name",{value:t,configurable:!0});import{ticketSource as A}from"../board/tickets.mjs";import{readManifest as h,withManifestLock as b,writeManifest as w}from"./boards.mjs";import{ROUTE_RE as E,routeFromName as I,routeRecord as u}from"./routes.mjs";import{repoService as O,validateGithub as L,validateBranch as P,validateRemote as _}from"./repos.mjs";import{startFailed as C,startSession as F,stopSession as G}from"./sessions.mjs";import{savedClient as M}from"./linear.mjs";const d=o((e,t=400)=>{throw Object.assign(new Error(e),{status:t})},"refuse");function T(e){(!e||typeof e!="object"||Array.isArray(e)||Object.keys(e).some(i=>!["name","github","remote","branch","purpose","tickets"].includes(i)))&&d("Send only name, github or remote, branch, purpose and tickets."),(typeof e.name!="string"||!e.name.trim()||e.name.length>60)&&d("Name must be 1\u201360 characters."),e.purpose!==void 0&&(typeof e.purpose!="string"||e.purpose.length>240)&&d("Purpose must be at most 240 characters.");const t=e.name.trim().replace(/\s+/g," "),s=I(t);E.test(s)||d("Name must contain a letter or digit for its route.");const{github:r,remote:l}=q(e),m=P(e.branch);let a=e.tickets===void 0?{source:"internal"}:e.tickets;l&&a?.source==="github"&&d("GitHub issues need a repo on GitHub; pick Internal or Linear.");let n;if(a?.source==="linear"&&typeof a=="object"&&Object.hasOwn(a,"new_project")){const{new_project:i,...g}=a;n=typeof i=="string"?i.trim():"",(!n||n.length>80)&&d("Name the new Linear project."),a={...g,project:n}}return{route:s,title:t,github:r,remote:l,branch:m,tickets:A(a),newProject:n,purpose:(e.purpose||"").trim().replace(/\s+/g," ")}}o(T,"details");function q(e){return e.github===void 0==(e.remote===void 0)&&d("Send the repo as github or as remote."),e.github!==void 0?{github:L(e.github)}:{remote:_(e.remote).remote}}o(q,"repoInput");const S=o((e,t)=>Object.hasOwn(e.sites||{},t)||Object.hasOwn(e.starting||{},t)||Object.hasOwn(e.closed||{},t),"taken"),N="That board route already exists. Reopen it from Closed boards if it is closed.",D=o(e=>`'${e.replaceAll("'","'\\''")}'`,"quote"),H=o(({purpose:e})=>`--template board --items data/wave.json${e?` --description ${D(e)}`:""}`,"joinFlags");function $(e){const t=String(e).split(`
2
+ `).map(s=>s.trim()).find(Boolean)||"claude did not start";return t.length>200?`${t.slice(0,199)}\u2026`:t}o($,"errorLine");async function R(e,t){await b(async()=>{const s=h(),r=u(s.starting,e);!r||u(s.sites,e)||(s.starting[e]=t.phase?{...r,phase:t.phase}:t.error?{...r,error:t.error}:{...r,session:t.session,label:t.name,since:new Date().toISOString(),repo:t.repo,...t.model?{model:t.model}:{}},await w(s))})}o(R,"recordStart");const v=o(({title:e,purpose:t,repo:s,github:r,remote:l,branch:m,tickets:a},n)=>({title:e,purpose:t,repo:s,...r?{github:r}:{remote:l},branch:m,tickets:a,phase:n.cloned(r||l)?"starting":"cloning",asked_at:new Date().toISOString()}),"startingRecord");async function x(e,{purpose:t,repo:s,github:r,remote:l,branch:m},{run:a,live:n,repos:i,changed:g}){let f;try{await i.ensure(r||l,m),await R(e,{phase:"starting"}),g();const c=await F(e,{repo:s,prompt:H({purpose:t}),run:a,live:n,clones:i.base});f={session:c.session,name:c.name,repo:c.repo,model:c.model}}catch(c){f={error:$(c.message)}}try{await R(e,f)}catch(c){return{error:$(`could not record the start: ${c.message}`)}}return f.error?{error:f.error}:{session:f.session}}o(x,"start");async function X(e,{run:t,live:s,repos:r=O(),changed:l=o(()=>{},"changed"),linear:m}={}){const a=T(e),{route:n,github:i,remote:g,newProject:f}=a;let{tickets:c}=a;const j=r.path(i||g),k=f===void 0?null:m||M();if(k){S(h(),n)&&d(N);try{c={...c,project:(await k.createProject(c.team,f)).id}}catch(p){d(p.message,p.status||502)}}return await b(async()=>{const p=h();S(p,n)&&d(N),p.starting={...p.starting,[n]:v({...a,repo:j,tickets:c},r)},await w(p)}),{route:n,started:x(n,{...a,repo:j},{run:t,live:s,repos:r,changed:l})}}o(X,"requestBoard");function y(e,t,s){const r=u(e.starting,t);return(!r||u(e.sites,t)||!C(r))&&d(`Only a board that didn\u2019t start can be ${s}.`),r}o(y,"failedStart");async function Y(e,{run:t,live:s,stop:r,repos:l=O(),changed:m=o(()=>{},"changed")}={}){const a=y(h(),e,"retried");a.branch||d("Only a board asked for with New board can be retried; make it again with New board.");try{await G(a,{live:s,stop:r})}catch(i){d(`Couldn\u2019t stop its earlier session: ${i.message}`,502)}const n=await b(async()=>{const i=h(),g=y(i,e,"retried");return i.starting[e]=v({...g,purpose:g.purpose||""},l),await w(i),i.starting[e]});return{route:e,started:x(e,n,{run:t,live:s,repos:l,changed:m})}}o(Y,"retryBoard");async function Z(e){return b(async()=>{const t=h();return y(t,e,"dismissed"),delete t.starting[e],Object.keys(t.starting).length||delete t.starting,await w(t),{route:e}})}o(Z,"dismissBoard");export{Z as dismissBoard,q as repoInput,X as requestBoard,Y as retryBoard};
@@ -0,0 +1,3 @@
1
+ #!/usr/bin/env node
2
+ var R=Object.defineProperty;var i=(t,a)=>R(t,"name",{value:a,configurable:!0});import D from"node:fs";const p=`http://127.0.0.1:${Number(process.env.KICKFLIP_HUB_PORT||4500)}`,f=10*6e4,O=f+3e4,d=2e3,r="The hub's permission hook failed; ask this as an open question on your board's card instead.",o=i(t=>({hookSpecificOutput:{hookEventName:"PermissionRequest",decision:{behavior:"deny",message:t}}}),"deny");let y=!1;function e(t){y||(y=!0,D.writeSync(1,JSON.stringify(t)+`
3
+ `),process.exit(0))}i(e,"finish"),process.on("uncaughtException",()=>e(o(r))),process.on("unhandledRejection",()=>e(o(r))),setTimeout(()=>e(o(r)),O).unref();const b=i(t=>new Promise(a=>setTimeout(a,t)),"sleep");async function P(){let t="";for await(const n of process.stdin)t+=n;const a=JSON.parse(t),{hookOutput:l,NO_ANSWER:_,UNREACHABLE:A,DROPPED:S,POLL_MS:E}=await import("./prompts.mjs");let h,w;try{const n=await fetch(`${p}/_hub/prompts`,{method:"POST",headers:{"content-type":"application/json"},body:t,signal:AbortSignal.timeout(5e3)});if(!n.ok)throw new Error(`hub ${n.status}`);const s=await n.json();if(s?.state==="answered")return a.tool_name!=="AskUserQuestion"&&s.answer?.choice==="allow"?e(l(a,s.answer)):e(o(r));({id:h,deadline:w}=s)}catch{return e(o(A))}const c=Math.min(Date.parse(w)||1/0,Date.now()+f);for(let n=c-Date.now();n>0;n=c-Date.now()){const s=Math.min(E,n);let u;try{u=await fetch(`${p}/_hub/prompts/${encodeURIComponent(h)}/wait?ms=${s}`,{signal:AbortSignal.timeout(s+1e4)})}catch{await b(Math.min(d,Math.max(0,c-Date.now())));continue}if(u.status===404)return e(o(S));let m=null;try{m=await u.json()}catch{}if(m?.state==="answered")return e(l(a,m.answer));u.ok||await b(Math.min(d,Math.max(0,c-Date.now())))}try{await fetch(`${p}/_hub/prompts/${encodeURIComponent(h)}`,{method:"DELETE",signal:AbortSignal.timeout(2e3)})}catch{}return e(o(_))}i(P,"main"),P().catch(()=>e(o(r)));
@@ -0,0 +1 @@
1
+ var d=Object.defineProperty;var i=(t,n)=>d(t,"name",{value:n,configurable:!0});import{execFile as f}from"node:child_process";const b="state,isDraft,reviewDecision,mergeable,mergeStateStatus,headRefOid,statusCheckRollup",w="/opt/homebrew/bin:/usr/local/bin:/usr/bin:/bin:/usr/sbin:/sbin",h=i(t=>String(t).trim().split(/\r?\n/)[0].slice(0,200),"firstLine");function p(t,n=process.env){return new Promise((o,s)=>{f("gh",t,{env:{...n,PATH:[n.PATH,w].filter(Boolean).join(":")},timeout:9e4,maxBuffer:16*1024*1024},(l,a,u)=>{if(l)return s(new Error(h(u||l.message)));try{o(JSON.parse(a))}catch(r){s(new Error(h(`unreadable output (${r.message})`)))}})})}i(p,"gh");async function E({repo:t,pr:n,since:o}){let s;try{s=await p(["pr","view",String(n),"--repo",t,"--json",b])}catch(m){throw new Error(`${t}#${n}: ${m.message}`)}const l=i(m=>p(["api","--paginate","--slurp",`repos/${t}/${m}`]).then(c=>c.flat()).catch(()=>[]),"list"),[a,u,r]=await Promise.all([l(`issues/${n}/comments?since=${encodeURIComponent(o)}`),l(`pulls/${n}/comments?since=${encodeURIComponent(o)}`),l(`pulls/${n}/reviews`)]);return{view:s,issueComments:a,reviewComments:u,reviews:r}}i(E,"fetchPR");function R({view:t,issueComments:n,reviewComments:o,reviews:s},{pr:l,since:a,me:u}){const r=i((e,g)=>({kind:g,id:e.id??null,login:e.user?.login??null,at:e.created_at??null,body:e.body??null}),"comment"),m=[...n.map(e=>({...r(e,"issue_comment"),url:e.html_url??null})),...o.map(e=>({...r(e,"review_comment"),path:e.path??null,line:e.line??null,in_reply_to:e.in_reply_to_id??null,url:e.html_url??null})),...s.map(e=>({kind:"review",id:e.id??null,login:e.user?.login??null,at:e.submitted_at??null,body:e.body??null,state:e.state??null,url:e.html_url??null}))].filter(e=>e.at!=null&&e.at>a).map(e=>({...e,who:e.login===u?/<!-- claude-bridge -->/.test(e.body??"")?"me":"user":/codex/.test(e.login)?"codex":/\[bot\]$|^linear/.test(e.login)?"bot":"reviewer"})).filter(e=>e.who!=="me"&&e.who!=="bot").sort((e,g)=>e.at<g.at?-1:e.at>g.at?1:0),c=t.statusCheckRollup??[];return{pr:Number(l),state:t.state??null,isDraft:t.isDraft??null,reviewDecision:t.reviewDecision??null,mergeable:t.mergeable??null,mergeStateStatus:t.mergeStateStatus??null,head:t.headRefOid?.slice(0,10)??null,failing:c.filter(e=>/FAILURE|ERROR/.test(e.conclusion??e.state??"")).map(e=>e.name??e.context??null),pending:c.filter(e=>(e.status??"COMPLETED")!=="COMPLETED").length,events:m}}i(R,"reportOf");async function $({env:t}={}){const n=await p(["api","user"],t);if(typeof n?.login!="string"||!n.login)throw new Error("gh api user: missing login");return n.login}i($,"githubLogin");export{b as PR_VIEW_FIELDS,E as fetchPR,$ as githubLogin,R as reportOf};
@@ -0,0 +1 @@
1
+ var l=Object.defineProperty;var s=(r,e)=>l(r,"name",{value:e,configurable:!0});import{execFile as d}from"node:child_process";const m=3*6e4,i=s(r=>new Promise((e,o)=>{d("ioreg",r,{timeout:5e3,maxBuffer:32*1024*1024},(t,n)=>t?o(t):e(n))}),"ioreg");async function u(r=i){try{const[e,o]=await Promise.all([r(["-n","Root","-d1"]),r(["-c","IOHIDSystem"])]),t=/"CGSSessionScreenIsLocked"\s*=\s*Yes/.test(e),n=[...o.matchAll(/"HIDIdleTime"\s*=\s*(\d+)/g)].map(c=>Number(c[1])/1e6);if(!n.length)return{atMac:!1,error:"ioreg reported no HIDIdleTime"};const a=Math.round(Math.min(...n));return{atMac:!t&&a<m,locked:t,idleMs:a}}catch(e){return{atMac:!1,error:String(e.message||e)}}}s(u,"readPresence");function I({run:r=i,ttl:e=5e3,clock:o=Date.now}={}){let t=null;return()=>((!t||o()-t.at>=e)&&(t={at:o(),reading:u(r)}),t.reading)}s(I,"presenceReader");const p=I();export{m as IDLE_LIMIT_MS,p as presence,I as presenceReader,u as readPresence};
@@ -0,0 +1,2 @@
1
+ var j=Object.defineProperty;var s=(e,t)=>j(e,"name",{value:t,configurable:!0});import S from"node:fs";import y from"node:os";import _ from"node:path";import E from"node:crypto";import{withFileLock as D}from"./locks.mjs";import{routeRecord as l}from"./routes.mjs";const f=_.join(y.homedir(),".claude","kickflip","prompts.json"),v=10*6e4,z=25e3,O=6e4,V="No answer from the owner; ask this as an open question on your board's card instead.",X="The hub can't be reached, so the owner can't answer this prompt; ask this as an open question on your board's card instead.",Y="The hub dropped this prompt; ask this as an open question on your board's card instead.",k="The owner denied this from the hub.",F="The owner skipped this question; ask it as an open question on your board's card instead.",m="AskUserQuestion",d={Edit:"edit",MultiEdit:"edit",Write:"write",Read:"read",NotebookEdit:"edit"},h=s((e,t)=>{const r=String(e??"").replace(/\s+/g," ").trim();return r.length>t?`${r.slice(0,t-1)}\u2026`:r},"cut"),w=s(e=>e?.file_path||e?.notebook_path||"","fileOf"),N=s(e=>{try{return new URL(e).host}catch{return String(e||"")}},"hostOf");function W(e,t={}){if(e==="Bash")return`run ${h(t.command,60)}`;if(l(d,e))return`${l(d,e)} ${_.basename(w(t))||"a file"}`;if(e==="WebFetch")return`fetch ${N(t.url)}`;if(e==="WebSearch")return`search the web for "${h(t.query,50)}"`;const r=/^mcp__(.+?)__(.+)$/.exec(e||"");return r?`use ${r[1]} ${r[2]}`:`use ${e}`}s(W,"actionOf");function T(e,t={}){if(e==="Bash")return String(t.command??"");if(l(d,e))return w(t);if(e==="WebFetch")return String(t.url??"");if(e==="WebSearch")return String(t.query??"");const r=JSON.stringify(t);return r==="{}"?"":h(r,600)}s(T,"detailOf");function P(e){const t=(Array.isArray(e?.permission_suggestions)?e.permission_suggestions:[]).filter(n=>n?.type==="addRules"&&n.behavior==="allow"&&Array.isArray(n.rules)&&n.rules.length).map(n=>({type:"addRules",rules:n.rules,behavior:"allow",destination:"session"}));if(t.length)return t;const r=e?.tool_name,o=e?.tool_input||{},i=r==="Bash"&&o.command?o.command:l(d,r)&&w(o).startsWith("/")?`/${w(o)}`:r==="WebFetch"&&o.url?`domain:${N(o.url)}`:null;return[{type:"addRules",rules:[i?{toolName:r,ruleContent:i}:{toolName:r}],behavior:"allow",destination:"session"}]}s(P,"alwaysRules");const R=s(e=>e.flatMap(t=>t.rules).map(t=>t.ruleContent?`${t.toolName}(${t.ruleContent})`:t.toolName).join(", "),"ruleText"),b=s(e=>Number.isInteger(e?.pid)&&e.pid>0&&typeof e.procStart=="string"&&Number.isFinite(Date.parse(`${e.procStart.trim().replace(/\s+/g," ")} UTC`))?{pid:e.pid,procStart:e.procStart}:null,"runtimeOf"),$=s((e,t)=>!!(b(e)&&b(t)&&e.pid===t.pid&&e.procStart===t.procStart),"sameRuntime"),p=s(e=>typeof e?.owner?.since=="string"&&e.owner.since?e.owner.since:null,"ownerSince"),x=s((e,t,r)=>!!(t?.owner?.session===r&&e.ownerSince&&e.ownerSince===p(t)),"sameOwner");function B(e,t){const r=t?.tool_name;if(r===m||!e||e.toolName!==r||typeof r!="string")return!1;const o=e.ruleContent;if(o===void 0)return!0;if(typeof o!="string"||!o)return!1;const i=t.tool_input||{};if(r==="Bash")return typeof i.command=="string"&&o===i.command;if(l(d,r))return o.startsWith("//")&&!/[\*?\[\]{}]/.test(o)&&o.slice(1)===w(i);if(r==="WebFetch")try{const n=new URL(i.url);return["http:","https:"].includes(n.protocol)&&o===`domain:${n.host}`}catch{return!1}return!1}s(B,"matchesRule");function Z(e,{running:t,boards:r}){let o=!1;for(const[i,n]of Object.entries(e.remembered||{})){const a=r.find(u=>n.route?u.route===n.route:u.owner?.session===i);!$(n.runtime,b(t.get(i)))||n.route&&!x(n,a,i)||a&&!p(a)?(delete e.remembered[i],o=!0):n.route===null&&a&&(n.route=a.route,n.ownerSince=p(a),o=!0)}return o}s(Z,"pruneRemembered");function ee(e,t,r){if(t.answer?.choice!=="always"||t.tool===m||!t.session||!t.permissions||!$(t.runtime,b(r.running.get(t.session))))return!1;const o=l(e.remembered,t.session),i=t.route||o?.route,n=i&&r.boards.find(c=>c.route===i)||!t.route&&r.boards.find(c=>c.owner?.session===t.session);if(t.route&&!x(t,n,t.session)||n&&(n.owner?.session!==t.session||!p(n)))return!1;e.remembered||={};const u=o&&$(o.runtime,t.runtime)&&(o.route?o.route===n?.route&&x(o,n,t.session):!0)?[...o.rules]:[];for(const c of t.permissions.flatMap(g=>g.rules))u.some(g=>g.toolName===c.toolName&&g.ruleContent===c.ruleContent)||u.push(c);return Object.defineProperty(e.remembered,t.session,{value:{route:n?.route||null,ownerSince:p(n),runtime:t.runtime,rules:u},enumerable:!0,writable:!0,configurable:!0}),!0}s(ee,"rememberRules");function te(e,t){return!t.session_id||t.tool_name===m||!l(e.remembered,t.session_id)?null:l(e.remembered,t.session_id).rules.some(r=>B(r,t))?{choice:"allow"}:null}s(te,"rememberedAnswer");function re(e,t=q()){return Object.entries(t.remembered||{}).filter(([,r])=>r.route===e).map(([r,o])=>({session:r,rules:o.rules.map(i=>R([{rules:[i]}]))}))}s(re,"rememberedFor");function oe(e,t){return{hookSpecificOutput:{hookEventName:"PermissionRequest",decision:t?.choice==="deny"?{behavior:"deny",message:t.message||k}:t?.choice==="always"&&e?.tool_name!==m?{behavior:"allow",updatedPermissions:P(e)}:e?.tool_name===m?{behavior:"allow",updatedInput:{...e.tool_input,answers:t?.answers||{}}}:{behavior:"allow"}}}}s(oe,"hookOutput");const A=s(e=>e&&e.startsWith(y.homedir()+"/")?`~${e.slice(y.homedir().length)}`:e||"","tilde");function C(e){return(Array.isArray(e?.questions)?e.questions:[]).filter(r=>typeof r?.question=="string").map(r=>({question:r.question,header:typeof r.header=="string"?r.header:"",multiSelect:r.multiSelect===!0,options:(Array.isArray(r.options)?r.options:[]).filter(o=>typeof o?.label=="string").map(o=>({label:o.label,description:typeof o.description=="string"?o.description:""}))}))}s(C,"questionsOf");function ne(e,{board:t,live:r,now:o=new Date}){const i=String(e.tool_name),n=i===m,a=n?null:P(e),u=o.toISOString();return{id:"p"+E.randomBytes(6).toString("hex"),session:String(e.session_id||""),route:t?.route||null,title:t?.title||A(e.cwd)||"A background session",cwd:String(e.cwd||""),tool:i,action:n?null:W(i,e.tool_input),detail:n?"":T(i,e.tool_input),rule:n?null:R(a),questions:n?C(e.tool_input):null,permissions:a,runtime:b(r),ownerSince:p(t),at:u,deadline:new Date(o.getTime()+v).toISOString(),seen:u,answer:null}}s(ne,"promptRecord");function se(e,t,r=new Date){const o=r.toISOString();if(e.questions){if(t?.choice==="deny")return{answer:{choice:"deny",message:F,at:o}};const i=t?.answers&&typeof t.answers=="object"?t.answers:{};for(const n of e.questions){const a=String(l(i,n.question)??"").split(n.multiSelect?", ":"\0").filter(Boolean);if(!a.length)return{error:`Pick an answer to "${n.question}".`};if(!a.every(u=>n.options.some(c=>c.label===u)))return{error:`"${l(i,n.question)}" isn't an option of "${n.question}".`}}return{answer:{choice:"allow",answers:Object.fromEntries(e.questions.map(n=>[n.question,String(l(i,n.question))])),at:o}}}return["allow","always","deny"].includes(t?.choice)?{answer:{choice:t.choice,...t.choice==="deny"?{message:k}:{},at:o}}:{error:"Pass choice allow, always or deny."}}s(se,"answerOf");function ie(e,{now:t=Date.now(),aliveSince:r=0}={}){return Object.values(e.prompts||{}).filter(o=>t-Date.parse(o.deadline)>O?!0:o.answer?t-Date.parse(o.answer.at)>O:t-Math.max(Date.parse(o.seen),r)>O)}s(ie,"expired");function q(){try{const e=JSON.parse(S.readFileSync(f,"utf8"));return{prompts:e?.prompts&&typeof e.prompts=="object"?e.prompts:{},remembered:e?.remembered&&typeof e.remembered=="object"?e.remembered:{}}}catch{return{prompts:{},remembered:{}}}}s(q,"readPrompts");function I(e){const t=`${f}.${process.pid}.tmp`;S.mkdirSync(_.dirname(f),{recursive:!0}),S.writeFileSync(t,JSON.stringify(e,null,2)+`
2
+ `),S.renameSync(t,f)}s(I,"savePrompts");const L=s(e=>D(f,e),"withPromptsLock");function ae(e){return L(()=>{const t=q(),r=e(t);return I(t),r})}s(ae,"updatePrompts");function ue(e,t){const r=new Set(t.filter(Boolean));for(const o of r)delete e.remembered[o];for(const[o,i]of Object.entries(e.prompts))r.has(i.session)&&delete e.prompts[o]}s(ue,"dropSessionPrompts");function ce(e,t=q()){return Object.values(t.prompts).filter(r=>!r.answer&&r.route===e).sort((r,o)=>r.at.localeCompare(o.at))}s(ce,"pendingFor");const le=s(({id:e,title:t,cwd:r,tool:o,action:i,detail:n,rule:a,questions:u,at:c})=>({id:e,title:t,cwd:A(r),tool:o,action:i,detail:n,rule:a,questions:u,at:c}),"shown");function me(e){const t=e.questions?.[0];return{title:t?`${e.title} asks: ${h(t.question,90)}`:`${e.title} wants to ${e.action}`,body:t?t.options.map(r=>r.label).join(" \xB7 ")+(e.questions.length>1?` (+${e.questions.length-1} more)`:""):h(e.detail,160)||`Answer in ${e.route?"Needs you":"the hub page"}.`,url:e.route?`/${e.route}/`:"/_hub/",tag:`prompt-${e.id}`}}s(me,"pushOf");export{k as DENIED,Y as DROPPED,O as GONE_AFTER,V as NO_ANSWER,z as POLL_MS,F as SKIPPED,X as UNREACHABLE,v as WAIT_MS,W as actionOf,P as alwaysRules,se as answerOf,ue as dropSessionPrompts,ie as expired,oe as hookOutput,B as matchesRule,ce as pendingFor,ne as promptRecord,Z as pruneRemembered,me as pushOf,q as readPrompts,ee as rememberRules,te as rememberedAnswer,re as rememberedFor,I as savePrompts,le as shown,ae as updatePrompts,L as withPromptsLock};
package/hub/push.mjs ADDED
@@ -0,0 +1,2 @@
1
+ var G=Object.defineProperty;var s=(t,e)=>G(t,"name",{value:e,configurable:!0});import m from"node:crypto";import u from"node:fs";import y from"node:path";import x from"node:os";import{validate as Y,pollingOf as F,flushAt as Z,hold as K,windowAt as tt,notifications as et,snoozeEnd as nt}from"./schedule.mjs";import{storedEnd as ot}from"./duration.mjs";import{presence as I}from"./presence.mjs";import{CODEX_SHARE_VALUES as rt,codexStatus as q,effectiveCodexShare as st}from"../shared/codex.mjs";const j=y.join(x.homedir(),".claude","kickflip","push"),z=y.join(j,"vapid.json"),O=y.join(j,"subs.json"),h=y.join(j,"queue.json"),D=h+".lock",J=s(t=>y.join(t,".claude","kickflip","settings.json"),"settingsFile"),P=s(t=>Buffer.from(t).toString("base64url"),"b64u"),$=s(t=>Buffer.from(t,"base64url"),"unb64u"),B=s((t,e)=>m.createHmac("sha256",t).update(e).digest(),"hmac"),l=s((t,e)=>{u.mkdirSync(j,{recursive:!0,mode:448});const n=`${t}.${process.pid}.tmp`;u.writeFileSync(n,JSON.stringify(e,null,2),{mode:384}),u.renameSync(n,t)},"writePrivate");function L(){try{return JSON.parse(u.readFileSync(z,"utf8"))}catch{}const t=m.generateKeyPairSync("ec",{namedCurve:"prime256v1"}).privateKey.export({format:"jwk"});return l(z,t),t}s(L,"vapidKey");function it(){const t=L();return P(Buffer.concat([Buffer.from([4]),$(t.x),$(t.y)]))}s(it,"publicKey");function g(t=O){try{return JSON.parse(u.readFileSync(t,"utf8"))}catch{return[]}}s(g,"readSubs");function bt(t,e,n=""){if(!/^https:\/\//.test(t?.endpoint||"")||!t.keys?.p256dh||!t.keys?.auth)throw Object.assign(new Error("bad subscription"),{status:400});const o=g().filter(i=>i.endpoint!==t.endpoint);return o.push({endpoint:t.endpoint,keys:{p256dh:t.keys.p256dh,auth:t.keys.auth},origin:e,agent:String(n).slice(0,200),added:new Date().toISOString()}),l(O,o),o.length}s(bt,"addSubscription");function Et(t){const e=g(),n=e.filter(o=>o.endpoint!==t);return n.length!==e.length&&l(O,n),n.length}s(Et,"removeSubscription");function jt(t){return g(t?y.join(t,".claude","kickflip","push","subs.json"):O).map(e=>({host:new URL(e.endpoint).host,agent:e.agent,added:e.added}))}s(jt,"subscriptions");const M=s(t=>!t.agent||/iPhone|iPad|Android/.test(t.agent),"onPhone");async function T(t,e=!1){const n=g();if(t!=="action"||e||!(await I()).atMac)return n;const o=n.filter(i=>!M(i));return o.length?o:n}s(T,"reach");async function Ot(){return{atMac:(await I()).atMac,desktops:g().filter(t=>!M(t)).length}}s(Ot,"pushRoute");function at(t,e){const n=$(t.keys.p256dh),o=m.createECDH("prime256v1"),i=o.generateKeys(),a=B($(t.keys.auth),o.computeSecret(n)),r=B(a,Buffer.concat([Buffer.from("WebPush: info\0"),n,i,Buffer.from([1])])),c=m.randomBytes(16),d=B(c,r),k=B(d,Buffer.from("Content-Encoding: aes128gcm\0")).subarray(0,16),b=B(d,Buffer.from("Content-Encoding: nonce\0")).subarray(0,12),E=m.createCipheriv("aes-128-gcm",k,b),v=Buffer.concat([E.update(Buffer.concat([Buffer.from(e),Buffer.from([2])])),E.final(),E.getAuthTag()]),f=Buffer.alloc(4);return f.writeUInt32BE(4096),Buffer.concat([c,f,Buffer.from([i.length]),i,v])}s(at,"encrypt");function ct(t){const e=L(),n=P(JSON.stringify({typ:"JWT",alg:"ES256"})),o=P(JSON.stringify({aud:new URL(t.endpoint).origin,exp:Math.floor(Date.now()/1e3)+12*3600,sub:t.origin||"mailto:kickflip@localhost"})),i=m.sign("sha256",Buffer.from(`${n}.${o}`),{key:m.createPrivateKey({key:e,format:"jwk"}),dsaEncoding:"ieee-p1363"});return`vapid t=${n}.${o}.${P(i)}, k=${it()}`}s(ct,"authorization");async function R(){try{const t=await fetch(`http://127.0.0.1:${Number(process.env.KICKFLIP_HUB_PORT||4500)}/_hub/needs.json?fresh=1`,{signal:AbortSignal.timeout(4e3)}),{total:e,sites:n}=await t.json();return{total:Number.isInteger(e)?e:void 0,sites:n}}catch{return{}}}s(R,"hubNeeds");const Bt=s(t=>_(g(),t),"sendAll");async function _(t,e,n){if(!t.length)return[];const o=JSON.stringify({...e,badge:(n||await R()).total}),i=await Promise.all(t.map(async r=>{try{const c=await fetch(r.endpoint,{method:"POST",headers:{authorization:ct(r),"content-encoding":"aes128gcm","content-type":"application/octet-stream",ttl:"86400",urgency:"high"},body:at(r,o),signal:AbortSignal.timeout(15e3)});return{sub:r,status:c.status,error:c.ok?void 0:(await c.text()).slice(0,300)}}catch(c){return{sub:r,status:0,error:String(c.message||c)}}})),a=new Set(i.filter(r=>r.status===404||r.status===410).map(r=>r.sub.endpoint));return a.size&&l(O,g().filter(r=>!a.has(r.endpoint))),i.map(r=>({host:new URL(r.sub.endpoint).host,agent:r.sub.agent,status:r.status,error:r.error,dropped:a.has(r.sub.endpoint)||void 0}))}s(_,"sendTo");const Q=s(t=>t.some(e=>e.status>=200&&e.status<300),"delivered");function w(t=x.homedir()){try{const e=JSON.parse(u.readFileSync(J(t),"utf8"));return e&&typeof e=="object"&&!Array.isArray(e)?e:{}}catch{return{}}}s(w,"readSettingsFile");function U(t,e=x.homedir()){const n=J(e);u.mkdirSync(y.dirname(n),{recursive:!0});const o=`${n}.${process.pid}.tmp`;u.writeFileSync(o,JSON.stringify(t,null,2)+`
2
+ `),u.renameSync(o,n)}s(U,"writeSettingsFile");const vt=s((t=q(),e=x.homedir())=>st(w(e).codexShare,t),"readCodexShare");function At(t,e=q()){if(!rt.includes(t))throw Object.assign(new Error("codex-share takes auto, always, or off"),{status:400});if(!e.usable)throw Object.assign(new Error(`${e.words} Run ${e.fix}, then choose again.`),{status:409});return U({...w(),codexShare:t}),t}s(At,"setCodexShare");const H=["opus","auto","sonnet"],Dt=s((t=x.homedir())=>{const e=w(t).claudeModel;return H.includes(e)?e:"auto"},"readClaudeModel");function Pt(t,e=x.homedir()){if(!H.includes(t))throw Object.assign(new Error("claude-model takes opus, auto or sonnet"),{status:400});return U({...w(e),claudeModel:t},e),t}s(Pt,"setClaudeModel");function N(t){return F(w(t))}s(N,"readSettings");function $t(){const t=w();if(!t.push)return null;const{push:e,...n}=t,{notifications:o,...i}=F(t);return U({...n,polling:i}),i}s($t,"migrateSettings");function ut(t){const{settings:e,error:n}=Y({notifications:N().notifications,...t});if(n)throw Object.assign(new Error(n),{status:400});const{push:o,...i}=w(),{notifications:a,...r}=e;return U({...i,polling:r,notifications:a}),e}s(ut,"saveSettings");async function Ut(t,e=new Date){const n=N(),o={...n.notifications};if("on"in t&&(o.on=t.on),"weekends_off"in t&&(o.weekends_off=t.weekends_off),"snooze"in t){const r=t.snooze==="off"?null:nt(e,t.snooze,n);if(r===null&&t.snooze!=="off")throw Object.assign(new Error("A snooze must end after now and within 30 days: <n>m, <n>h, <n>d, <n>w, tonight, morning, monday or a date and time; or forever or off."),{status:400});o.snoozed_until=r&&ot(r)}const{notifications:i}=ut({...n,notifications:o}),a=i.on?0:await S(()=>{const{items:r}=p();return r.length&&l(h,{items:[]}),r.length});return{notifications:i,dropped:a}}s(Ut,"setNotifications");function p(){try{const t=JSON.parse(u.readFileSync(h,"utf8"));return{items:Array.isArray(t.items)?t.items:[]}}catch{return{items:[]}}}s(p,"readQueue");async function S(t){u.mkdirSync(j,{recursive:!0,mode:448});const e=Date.now()+1e4;for(;;)try{u.mkdirSync(D);break}catch(n){if(n.code!=="EEXIST")throw n;try{if(Date.now()-u.statSync(D).mtimeMs>3e4){u.rmdirSync(D);continue}}catch{}if(Date.now()>e)throw new Error("push queue is locked; try again");await new Promise(o=>setTimeout(o,50))}try{return t()}finally{try{u.rmdirSync(D)}catch{}}}s(S,"withQueueLock");function ft(t=new Date){const e=N(),{items:n}=p(),o=K(t,e);return{settings:e,window:tt(t,e),held:o!==null,hold:o,count:n.length,nextFlushAt:n.length?Z(t,e):null}}s(ft,"queueStatus");const V=["action","prompt","fault"];class dt extends Error{static{s(this,"PushKindError")}}const lt=s(t=>V.includes(t)?null:`a push needs --kind ${V.join("|")}${t===void 0||t===!0?"":`, not "${t}"`}; news goes on the board, not in a push`,"pushKindError");async function Nt(t,{kind:e,urgent:n=!1,immediate:o=!1,at:i=new Date}={}){const a=lt(e);if(a)throw new dt(a);if(!g().length)return{noDevice:!0};const r={title:t.title,body:t.body||"",url:t.url||"/",tag:t.tag||void 0},c=K(i,N());if(c?.why==="off")return{dropped:!0};const d=c!==null;let k=!1;if(n||o&&!d){const f=await _(await T(e,n),r);if(Q(f))return r.tag&&await S(()=>l(h,{items:p().items.filter(A=>A.tag!==r.tag)})),{sent:f};if(n)return{sent:f};k=!0}const b={id:m.randomUUID(),...r,kind:e,queuedAt:i.toISOString()},E=await S(()=>{const f=p(),A=b.tag?f.items.filter(X=>X.tag!==b.tag):f.items,W=A.length<f.items.length;return f.items=[...A,b],l(h,f),{replaced:W}}),v=ft(i);return{queued:{...E,count:v.count,nextFlushAt:v.nextFlushAt,held:d,hold:c,sendFailed:k}}}s(Nt,"deliver");async function _t(t,e,n){return!t||!p().items.some(o=>o.tag===t)?null:S(()=>{const o=p(),i=o.items.find(a=>a.tag===t);return i?(i.body=e(i.body||""),n&&(i.url=n),l(h,o),i.body):null})}s(_t,"reviseQueued");async function Ct(t,e){const n=new Set(t),o=s(i=>n.has(i.tag)&&typeof i.url=="string"&&i.url.startsWith(`/${e}/`),"matches");return p().items.some(o)?S(()=>{const{items:i}=p(),a=i.filter(c=>!o(c)),r=i.length-a.length;return r&&l(h,{items:a}),r}):0}s(Ct,"dropQueued");let C=null;function Ft(){return C||=(async()=>{const t=await S(()=>{const a=p();return a.items.length&&l(h,{items:[]}),a.items});if(!t.length)return null;const e=[],n=[],o=[],i=await R();for(const a of et(t,i.sites)){const r=await _(await T(a.kind),{title:a.title,body:a.body,url:a.url,tag:a.tag},i);o.push(...r),(Q(r)?e:n).push(a)}return n.length&&await S(()=>{const a=p(),r=new Set(a.items.map(d=>d.tag).filter(Boolean)),c=t.filter(d=>n.some(k=>k.members.includes(d.id))&&!(d.tag&&r.has(d.tag)));l(h,{items:[...c,...a.items]})}),{count:t.length,sent:e.length,ok:!n.length,results:o}})().finally(()=>{C=null}),C}s(Ft,"flushQueue");export{H as CLAUDE_MODEL_VALUES,V as PUSH_KINDS,dt as PushKindError,bt as addSubscription,Nt as deliver,Ct as dropQueued,Ft as flushQueue,$t as migrateSettings,it as publicKey,lt as pushKindError,Ot as pushRoute,ft as queueStatus,Dt as readClaudeModel,vt as readCodexShare,N as readSettings,w as readSettingsFile,Et as removeSubscription,_t as reviseQueued,ut as saveSettings,Bt as sendAll,Pt as setClaudeModel,At as setCodexShare,Ut as setNotifications,jt as subscriptions,U as writeSettingsFile};
@@ -0,0 +1,2 @@
1
+ var T=Object.defineProperty;var s=(t,r)=>T(t,"name",{value:r,configurable:!0});import f from"node:fs";import m from"node:os";import y from"node:path";import{execFile as N}from"node:child_process";import{githubLogin as P}from"./pr-report.mjs";import{codexStatusReader as w}from"../shared/codex.mjs";import{linearStatus as R}from"./linear.mjs";import{readSettings as L,subscriptions as O}from"./push.mjs";import{tailscaleState as C,tailscaleInstallHint as I}from"../bin/install.mjs";const x=[["github","GitHub","GitHub polling and PR reports wait on this."],["codex","Codex","Codex implementing and reviewing wait on this; Codex share stays Off."],["linear","Linear","Linear moves and picking Linear for a board\u2019s tickets wait on this."],["slack","Slack","Reviewer DMs and digests wait on this."],["tailscale","Tailscale","Try builds wait on this."],["notifications","Notifications","Pushes wait on this."]],k=s(t=>y.join(t,".claude","kickflip","integrations.json"),"reportFile");function F(t=m.homedir()){try{return JSON.parse(f.readFileSync(k(t),"utf8")).slack||null}catch{return null}}s(F,"readSlackReport");function Q(t,{home:r=m.homedir(),now:i=new Date,route:l=null,title:o="Board"}={}){const e=k(r),n={ready:t,at:i.toISOString(),route:l,title:o};f.mkdirSync(y.dirname(e),{recursive:!0});const a=`${e}.${process.pid}.tmp`;return f.writeFileSync(a,JSON.stringify({slack:n},null,2)+`
2
+ `,{mode:384}),f.renameSync(a,e),n}s(Q,"reportSlack");const H=s((t,r)=>{const i=Math.floor((r-Date.parse(t))/6e4);return i<1?"just now":i<60?`${i} min ago`:i<1440?`${Math.floor(i/60)} h ago`:`${Math.floor(i/1440)} days ago`},"age"),u=s((t,r,i)=>({settings:t,label:`Hub settings \u203A ${r}`,text:i}),"settingsFix");function b(t,{now:r=Date.now(),platform:i=process.platform}={}){const l=s((o,e)=>{switch(o){case"github":return e?{ready:!0,detail:`gh logged in as ${e}`}:{ready:!1,missing:"gh isn't logged in on the host",fix:{command:"gh auth login"}};case"codex":return e.usable?{ready:!0,detail:"Installed and logged in"}:{ready:!1,missing:e.words,fix:{command:e.fix}};case"linear":return e.set?e.refused?{ready:!1,missing:"Linear refused the key",fix:u("linear","Linear","Paste a new one in")}:{ready:!0,detail:`Key set \xB7 ${e.workspace} workspace`}:{ready:!1,missing:"No Linear key set",fix:u("linear","Linear","Add one in")};case"slack":{const n=e?`${e.title}\u2019s session reported ${e.ready?"a":"no"} Slack connector, ${H(e.at,r)}`:"No board session has reported yet";return e?.ready?{ready:!0,detail:n}:{ready:!1,missing:n,fix:{text:"Add Slack to Claude\u2019s connectors. The next board session to start reports it."}}}case"tailscale":{let n,a;return e.installed?e.signedIn?e.pointsAtHub?e.operatorChecked&&e.operator!==e.username&&(n=`Tailscale operator is ${e.operator||"none"} (expected ${e.username})`,a="sudo tailscale set --operator=$USER"):(n="Tailscale / is not serving the hub",a="kickflip doctor --fix"):(n="Tailscale isn't signed in",a="tailscale up"):(n="Tailscale isn't installed on the host",a=I(i)),n?{ready:!1,missing:n,fix:{command:a}}:{ready:!0,detail:`Serving ${e.dns}${e.operatorChecked?" \xB7 operator "+e.operator:""}`}}case"notifications":return e.on&&e.devices?{ready:!0,detail:`Pushes on \xB7 ${e.devices} device${e.devices===1?"":"s"}`}:{ready:!1,missing:e.on?"No device has turned on notifications":"Pushes are off",fix:u("notifications","Notifications","Turn on notifications in")}}},"row");return x.filter(([o])=>Object.hasOwn(t,o)).map(([o,e,n])=>({id:o,name:e,...l(o,t[o]),waits:n}))}s(b,"readinessRows");const j=s(t=>(r,i)=>new Promise(l=>{N(r,i,{env:t,timeout:3e3},(o,e)=>l({status:o?1:0,stdout:e}))}),"asyncRun");function D({home:t=m.homedir(),env:r=process.env,now:i=Date.now,cacheMs:l=6e4,probes:o={}}={}){const e=w({home:t,env:r,now:i,cacheMs:l}),n={github:s(()=>P({env:r}).catch(()=>null),"github"),codex:e,linear:s(()=>R(t),"linear"),slack:s(()=>F(t),"slack"),tailscale:s(()=>C({run:j(r),port:Number(r.KICKFLIP_HUB_PORT||4500)}),"tailscale"),notifications:s(()=>({on:L(t).notifications.on,devices:O(t).length}),"notifications"),...o},a=new Map;async function g(c,d){if(!["github","tailscale"].includes(c))return n[c]({fresh:d});const p=a.get(c);if(!d&&p&&i()-p.at<l)return p.value;const h=Promise.resolve().then(()=>n[c]());return a.set(c,{at:i(),value:h}),h}s(g,"check");const $=s(async({fresh:c=!1}={})=>b(Object.fromEntries(await Promise.all(x.map(async([d])=>[d,await g(d,c)]))),{now:i()}),"rows");async function S(c){const[d]=b({[c]:await g(c,!1)},{now:i()});return d.ready?null:d.waits.replace("this",`${d.name}: ${d.missing.replace(/\.$/,"")}`)}return s(S,"waiting"),{rows:$,waiting:S}}s(D,"readinessReader");const A=w(),V=D({probes:{codex:A}});export{A as readCodex,F as readSlackReport,V as readiness,D as readinessReader,b as readinessRows,Q as reportSlack};
package/hub/repos.mjs ADDED
@@ -0,0 +1,3 @@
1
+ var X=Object.defineProperty;var i=(t,e)=>X(t,"name",{value:e,configurable:!0});import y from"node:fs";import h from"node:path";import{spawn as Y}from"node:child_process";import{reposDir as E}from"../shared/repos.mjs";import{withFileLock as Z}from"./locks.mjs";import{storedPath as G}from"../bin/install.mjs";const m=i((t,e=400)=>{throw Object.assign(new Error(t),{status:e})},"refuse");function S(t){return(typeof t!="string"||t.length>140||!/^[a-z\d](?:[a-z\d-]{0,37}[a-z\d])?\/[a-z\d_.-]{1,100}$/i.test(t)||[".","..",".git"].includes(t.split("/")[1].toLowerCase()))&&m("Repo must be owner/name on GitHub."),t}i(S,"validateGithub");function O(t){return(typeof t!="string"||!t||t.length>255||t==="@"||t.startsWith("-")||/[\s\x00-\x1f\x7f~^:?*\[\\]/.test(t)||t.includes("..")||t.includes("@{")||t.split("/").some(e=>!e||e.startsWith(".")||e.endsWith(".")||e.endsWith(".lock")))&&m("Choose a valid branch name."),t}i(O,"validateBranch");const b=i((t,e)=>{try{return t(e),!0}catch{return!1}},"valid");function tt(t){const e=/^(?:git@github\.com:|https:\/\/github\.com\/|ssh:\/\/git@github\.com\/)([^/\s]+\/[^/\s]+?)(?:\.git)?\/?$/i.exec(t.trim());return e?z(e[1]):null}i(tt,"githubFromOrigin");const I=/^[a-z\d](?:[a-z\d-]*[a-z\d])?(?:\.[a-z\d](?:[a-z\d-]*[a-z\d])?)+$/i,N=/^[\w~][\w.~-]{0,99}$|^\.[\w~-][\w.~-]{0,98}$/;function A(t){if(typeof t!="string")return null;const e=t.trim().replace(/\/+$/,""),n=/^https:\/\/([^/@\s?#]+?)(?::\d{1,5})?\/([^\s?#]+)$/i.exec(e)||/^ssh:\/\/(?:[^@/:\s]+@)?([^/@:\s?#]+)(?::\d{1,5})?\/([^\s?#]+)$/i.exec(e)||/^[^@/:\s]+@([^/@:\s?#]+):([^\s?#]+)$/.exec(e);if(!n||e.length>400||!I.test(n[1])||/^(?:www\.)?github\.com$/i.test(n[1]))return null;const r=n[2].replace(/^\/+/,"").replace(/\.git$/i,"").split("/");return r.length>10||!r.every(o=>N.test(o)&&o!=="..")?null:{remote:e,host:n[1].toLowerCase(),path:r.join("/")}}i(A,"remoteFromInput");const M="Paste a git link: https://host/path, ssh://host/path or user@host:path.",k=i(t=>A(t)||m(M),"validateRemote"),$=i(t=>typeof t=="string"&&t.includes(":"),"isRemote"),x=i(t=>$(t)?(({host:e,path:n})=>[e,...n.toLowerCase().split("/")])(k(t)):S(t).toLowerCase().split("/"),"repoParts"),B=i(t=>$(t)?(({host:e,path:n})=>`${e}/${n}`)(k(t)):S(t),"repoName"),W=i((t,e)=>{try{return x(t).join("/")===x(e).join("/")}catch{return!1}},"sameRepo");function z(t){if(typeof t!="string")return null;const e=t.trim(),n=/^(?:https?:\/\/(?:www\.)?github\.com\/|git@github\.com:|ssh:\/\/git@github\.com\/)([^/\s?#]+\/[^/\s?#]+)(?:\/[^\s]*)?(?:[?#][^\s]*)?$/i.exec(e),r=n?n[1].replace(/\.git$/i,""):e;try{return S(r)}catch{return null}}i(z,"githubFromInput");async function yt(t,{repos:e,manifest:n,sendJSON:r}){try{return r(200,await e.lookup(t.searchParams.get("github")??t.searchParams.get("remote"),n))}catch(o){return r(o.status||502,{error:o.message})}}i(yt,"repoLookupRoute");function et(t,e,n={}){return new Promise(r=>{const o=Y(t,e,{...n,shell:!1,stdio:["ignore","pipe","pipe"]});let u="",g="";o.stdout.on("data",w=>{u+=w}),o.stderr.on("data",w=>{g+=w}),o.on("error",w=>r({status:1,stdout:u,stderr:w.message})),o.on("close",w=>r({status:w,stdout:u,stderr:g}))})}i(et,"runRepo");function C(t,e=E()){const n=x(t),r=h.resolve(e),o=h.resolve(r,...n);o.startsWith(`${r}${h.sep}`)||m("Clone path must stay inside .repos.");let u=r;for(const w of["",...n])w&&(u=h.join(u,w)),y.lstatSync(u,{throwIfNoEntry:!1})?.isSymbolicLink()&&m("Clone path must stay inside .repos; symlinks are refused.");let g=r;for(;!y.existsSync(g);)g=h.dirname(g);return y.realpathSync(g)!==g&&m("Clone path must stay inside .repos; symlinks are refused."),o}i(C,"repoPath");const P=i(t=>y.lstatSync(h.join(t,".git"),{throwIfNoEntry:!1})?.isDirectory()===!0,"hasGit");function $t(t,e=E()){let n,r,o;try{n=y.realpathSync(t),r=y.realpathSync(e),o=h.relative(r,n).split(h.sep)}catch{return!1}if(o.length===2&&b(S,o.join("/")))return P(n);if(o.length<2||o.length>11||!I.test(o[0])||!o.slice(1).every(u=>N.test(u)&&u!==".."))return!1;for(let u=2;u<o.length;u++)if(P(h.join(r,...o.slice(0,u))))return!1;return P(n)}i($t,"isClone");const st=/^(?:Cloning into |remote: (?:Enumerating|Counting|Compressing|Total) |Receiving objects:|Resolving deltas:|Updating files:)/;function nt(t,e){return(String(t||"").split(/\n\s*\n/).map(r=>r.split(/[\r\n]+/).map(o=>o.trim()).filter(o=>o&&!st.test(o))).find(r=>r.length)?.at(-1)||e).replace(/(\w+:\/\/)[^/\s@]+@/g,"$1")}i(nt,"errorLine");const _=i((t,e)=>nt(t.stderr||t.stdout,e),"failure"),ot=i(async(t,e,n,r)=>{const o=await t(e,n,r);return o.status!==0&&m(_(o,`${e} failed`),502),o.stdout},"checked"),R=i(async(t,e)=>JSON.parse(await ot(t,"gh",e)),"ghJSON");async function D(t,e,n){y.lstatSync(h.join(e,".git"),{throwIfNoEntry:!1})?.isDirectory()||m("The clone path must have a real .git metadata directory; symlinks are refused.");const r=await t("git",["-C",e,"rev-parse","--show-toplevel"]);(r.status!==0||h.resolve(r.stdout.trim())!==y.realpathSync(e))&&m("The clone path must be the root of a Git work tree.");const o=await t("git",["-C",e,"remote","get-url","origin"]);o.status===0&&($(n)?W(o.stdout.trim(),n):tt(o.stdout)?.toLowerCase()===n.toLowerCase())||m(`The clone path origin must be ${$(n)?B(n):`github.com/${n}`}.`)}i(D,"verifyClone");const H=i((t,e)=>t("git",["-c","credential.helper=",...e],{env:{...process.env,GIT_TERMINAL_PROMPT:"0"}}),"publicGit"),rt=i(t=>["-c","credential.helper=","-c","credential.https://github.com.helper=!gh auth git-credential",...t],"githubGitArgv"),it=i((t,e)=>t("git",rt(e),{env:{...process.env,GIT_TERMINAL_PROMPT:"0"}}),"githubGit"),at=i((t=process.env)=>({...t,GIT_TERMINAL_PROMPT:"0",GIT_SSH_COMMAND:t.GIT_SSH_COMMAND||"ssh -o BatchMode=yes"}),"remoteGitEnv"),L=i((t,e)=>t("git",e,{env:at()}),"remoteGit"),F=i(t=>$(t)?L:it,"gitFor"),ct=i(t=>$(t)?t:`https://github.com/${t}.git`,"cloneURL"),lt=/not found|authentication failed|could not read username|invalid username or password|permission denied|terminal prompts disabled|\b403\b/i;function ut(t,{github:e="",remote:n="",repo:r}){const o=String(t||"").split(`
2
+ `).map(v=>v.trim()).filter(Boolean),u=(o.find(v=>/^remote:/i.test(v))||o.at(-1)||"git failed without a message").replace(/(\w+:\/\/)[^/\s@]+@/g,"$1"),g=A(n),w=g?`check git can reach ${g.host}: git ls-remote ${g.remote}`:e&&lt.test(o.join(`
3
+ `))?`git's github.com login can't see ${e}; run gh auth setup-git`:`run git -C ${r} fetch origin to see the whole error`;return{error:u,hint:w}}i(ut,"gitReachFault");const T=i((t,e)=>{const{error:n,hint:r}=ut(t.stderr||t.stdout,{remote:e,repo:""});return`${n.replace(/[.\s]*$/,".")} ${r[0].toUpperCase()}${r.slice(1)}.`},"reachError");async function U(t,e,n,r){const o=await F(n)(t,["-C",e,"fetch","--no-tags","origin",`+refs/heads/${r}:refs/remotes/origin/${r}`]);o.status!==0&&m(`Could not fetch branch "${r}" from ${B(n)}: ${_(o,"git fetch failed")}`,502)}i(U,"fetchBranch");function vt({run:t=et,base:e=E(),now:n=Date.now}={}){let r,o;const u=new Map,g=i(s=>({github:s.nameWithOwner,private:s.isPrivate,pushedAt:s.pushedAt||"",defaultBranch:s.defaultBranchRef?.name||"",description:s.description||""}),"repoRow"),w=i((s,l)=>{const a=s.github||s.remote,c=C(a,e),p=Object.entries(l.sites||{}).filter(([,f])=>W(f.github||f.remote,a)&&typeof f.owner?.repo=="string"&&G(h.resolve(f.owner.repo)).toLowerCase()===G(c).toLowerCase()).map(([f,d])=>({route:f,title:d.title||f}));return{...s,clone:c,cloned:y.lstatSync(h.join(c,".git"),{throwIfNoEntry:!1})?.isDirectory()||!1,shared:p}},"decorate");async function v(s,l){const a=await s(t,["ls-remote","--symref",l,"HEAD","refs/heads/*"]);if(a.status!==0)return{failed:a};const c=a.stdout,p=[...new Set([...c.matchAll(/^[\da-f]+\s+refs\/heads\/(.+)$/gm)].map(d=>d[1]).filter(d=>b(O,d)))],f=/^ref:\s+refs\/heads\/(.+)\s+HEAD$/m.exec(c)?.[1];return{defaultBranch:p.includes(f)?f:p[0]||"",branches:p}}i(v,"lsRemote");async function J(s){const l=await v(H,`https://github.com/${s}.git`);return l.failed?null:l}i(J,"remoteBranches");async function q(s){const l=await v(L,s);l.failed&&m(T(l.failed,s),404);const{host:a,path:c}=k(s);return{remote:s,host:a,name:`${a}/${c}`,github:"",pushedAt:"",defaultBranch:l.defaultBranch,description:""}}i(q,"findRemote");async function K(s){const l=await t("gh",["repo","view",s,"--json","nameWithOwner,isPrivate,description,pushedAt,defaultBranchRef"]);if(l.status===0){const c=JSON.parse(l.stdout);return S(c.nameWithOwner),g(c)}return(await H(t,["ls-remote","--heads",`https://github.com/${s}.git`])).status!==0?null:{github:s,private:!1,pushedAt:"",defaultBranch:"",description:""}}i(K,"find");async function Q(){if((await t("gh",["auth","status"])).status!==0)return{signedIn:!1,repos:[]};const l=await R(t,["api","user"]),a=await R(t,["api","user/orgs","--paginate","--slurp"]),c=[...new Set([l.login,...a.flat().map(d=>d.login)])],p=await Promise.all(c.map(d=>R(t,["repo","list",d,"--limit","100000","--json","nameWithOwner,isPrivate,pushedAt,defaultBranchRef,description"]))),f=new Map;for(const d of p.flat())b(S,d.nameWithOwner)&&f.set(d.nameWithOwner.toLowerCase(),g(d));return{signedIn:!0,login:l.login,owners:c,repos:[...f.values()].sort((d,j)=>(j.pushedAt||"").localeCompare(d.pushedAt||"")||d.github.localeCompare(j.github))}}return i(Q,"discover"),{base:h.resolve(e),path:i(s=>C(s,e),"path"),cloned:i(s=>y.lstatSync(h.join(C(s,e),".git"),{throwIfNoEntry:!1})?.isDirectory()||!1,"cloned"),async list(s={},{fresh:l=!1}={}){return(l||!r||n()-r.at>=6e4)&&(o||=Q().then(a=>(r={at:n(),value:a},a)).finally(()=>{o=null}),await o),{...r.value,repos:r.value.repos.map(a=>w(a,s))}},async lookup(s,l={}){const a=z(s),c=a?null:A(s);!a&&!c&&m($(s)?M:"Repo must be owner/name on GitHub.");const p=a?a.toLowerCase():x(c.remote).join("/");let f=u.get(p);if(!f||n()-f.at>=6e4){for(const[j,V]of u)n()-V.at>=6e4&&u.delete(j);f={at:n(),pending:a?K(a):q(c.remote)},u.set(p,f),f.pending.catch(()=>{u.get(p)===f&&u.delete(p)})}const d=await f.pending;return d||m("Not found, or you don't have access.",404),w({...d,listed:!1},l)},async branches(s){if($(s)){k(s);const a=await v(L,s);return a.failed&&m(T(a.failed,s),502),a}const l=S(s);try{const a=await R(t,["api",`repos/${l}`]),c=await R(t,["api",`repos/${l}/branches?per_page=100`,"--paginate","--slurp"]);return{defaultBranch:a.default_branch,branches:c.flat().map(p=>p.name).filter(p=>b(O,p))}}catch(a){const c=await J(l);if(!c)throw a;return c}},async ensure(s,l="main"){const a=C(s,e);return O(l),y.mkdirSync(h.dirname(a),{recursive:!0}),Z(h.join(h.resolve(e),".locks",...x(s)),async()=>{if(C(s,e),y.existsSync(a))return await D(t,a,s),await U(t,a,s,l),a;const c=y.mkdtempSync(h.join(h.resolve(e),".clone-"));try{const p=await F(s)(t,["clone",ct(s),h.join(c,"repo")]);p.status!==0&&m($(s)?T(p,s):_(p,"git clone failed"),502),await D(t,h.join(c,"repo"),s),await U(t,h.join(c,"repo"),s,l),C(s,e),y.renameSync(h.join(c,"repo"),a)}finally{y.rmSync(c,{recursive:!0,force:!0})}return a},{timeout:10*6e4,heartbeat:!0})}}}i(vt,"repoService");export{ut as gitReachFault,z as githubFromInput,tt as githubFromOrigin,rt as githubGitArgv,$t as isClone,$ as isRemote,A as remoteFromInput,at as remoteGitEnv,yt as repoLookupRoute,B as repoName,x as repoParts,C as repoPath,vt as repoService,et as runRepo,O as validateBranch,S as validateGithub,k as validateRemote};
@@ -0,0 +1 @@
1
+ var $=Object.defineProperty;var r=(s,e)=>$(s,"name",{value:e,configurable:!0});import{listBoards as w,readManifest as h,withManifestLock as k,writeManifest as g}from"./boards.mjs";import{routeRecord as l,validRoute as B}from"./routes.mjs";import{sessionOf as M,stopSession as S,withSessionLock as T}from"./sessions.mjs";const t=r((s,e=400)=>{throw Object.assign(new Error(s),{status:e})},"refuse"),j=r(s=>!!(Array.isArray(s?.cmd)&&s.cmd.length&&s.port&&s.owner?.session&&s.owner.repo),"restartable");async function R(s,{serverUp:e,live:c,stop:m,run:p}={}){return B(s)||t("Choose a board route."),T(s,async({resume:b})=>{const n=h();w(n).find(o=>o.route===s)||t("That board is not open.",404);const a=l(n.sites,s),i=a.title||s;j(a)||t(`${i} has no server the hub runs or no session to resume.`,409);const f=w(n).filter(o=>o.route!==s&&o.owner?.session===a.owner.session);f.length&&t(`This session is also recorded by ${f.map(o=>`${o.title} (/${o.route})`).join(", ")}; restart refused.`,409),await S(M(n,s),{live:c,stop:m}),await k(async()=>{const o=h(),d=l(o.sites,s);d||t("That board is not open.",404),d.restart_at=Date.now(),await g(o)});const y=await e(s),{session:u}=await b({run:p,live:c});return y||t(`${i}\u2019s session is back, but its server hasn\u2019t answered yet. See kickflip logs ${s}.`,504),{route:s,title:i,session:u}})}r(R,"restartBoard");export{R as restartBoard,j as restartable};
package/hub/routes.mjs ADDED
@@ -0,0 +1 @@
1
+ var n=Object.defineProperty;var t=(e,o)=>n(e,"name",{value:o,configurable:!0});const a=/^[a-z0-9][a-z0-9-]{0,40}$/,p=t(e=>typeof e=="string"&&a.test(e),"validRoute");function r(e){return e.toLowerCase().replace(/[^a-z0-9]+/g,"-").replace(/^-+|-+$/g,"").slice(0,41).replace(/-+$/,"")}t(r,"routeFromName");const s=t((e,o)=>Object.hasOwn(e||{},o)?e[o]:void 0,"routeRecord"),x=t((e,o)=>o&&r(o)===e?o:e,"joinName");export{a as ROUTE_RE,x as joinName,r as routeFromName,s as routeRecord,p as validRoute};
package/hub/rules.mjs ADDED
@@ -0,0 +1 @@
1
+ var I=Object.defineProperty;var o=(t,r)=>I(t,"name",{value:r,configurable:!0});import h from"node:fs";import y from"node:path";import j from"node:os";import{readSettingsFile as S,writeSettingsFile as E}from"./push.mjs";const w=["owner","claude"],$=400,b=50;class D extends Error{static{o(this,"RuleError")}status=400}const s=o(t=>{throw new D(t)},"fail");function p(t,r="a rule"){const e=typeof t=="string"?t.replace(/\s+/g," ").trim():"";return e||s(`${r} must be non-empty text`),e.length>$&&s(`${r} has ${e.length} characters; a rule takes at most ${$}`),e}o(p,"ruleText");const J=o(t=>typeof t?.id=="string"&&typeof t.text=="string"&&t.text.trim()!=="","isRule"),L=o(t=>Array.isArray(t?.rules)?t.rules.filter(J):[],"rulesOf"),R=o(()=>"r"+Date.now().toString(36)+Math.random().toString(36).slice(2,5),"newId"),x=o(t=>typeof t=="string"&&/^[\w-]{1,40}$/.test(t)?t:s(`a rule id is letters, digits, "_" and "-", not "${t}"`),"ruleId"),O=o((t,r=1)=>t.length+r<=b?t:s(`a list takes at most ${b} rules`),"room");function v(t,{text:r,by:e="owner"},i=new Date){w.includes(e)||s(`--by takes ${w.join(" or ")}`);const u={id:R(),text:p(r,"--text"),by:e,at:i.toISOString()};return{rules:[...O(t),u],rule:u}}o(v,"addRule");function B(t,r,e,i=new Date){const u=t.find(a=>a.id===x(r))||s(`no rule "${r}"`),l={...u,text:p(e,"--text"),at:i.toISOString()};return{rules:t.map(a=>a===u?l:a),rule:l}}o(B,"editRule");function H(t,r){const e=t.find(i=>i.id===x(r))||s(`no rule "${r}"`);return{rules:t.filter(i=>i!==e),rule:e}}o(H,"removeRule");function X(t,r,e=[],i=new Date){Array.isArray(r)||s("rules must be a JSON array"),(!Array.isArray(e)||e.some(n=>typeof n!="string"))&&s("base must be a JSON array of rule ids");const u=new Map(t.map(n=>[n.id,n])),l={added:[],edited:[],removed:[]},a=new Set,m=r.map((n,f)=>{(!n||typeof n!="object"||Object.keys(n).some(g=>g!=="id"&&g!=="text"))&&s(`rules[${f}] takes only id and text`);const c=p(n.text,`rules[${f}].text`),d=n.id===void 0?null:u.get(x(n.id));return!d||a.has(d.id)?(l.added.push(c),{id:R()+f.toString(36),text:c,by:"owner",at:i.toISOString()}):(a.add(d.id),d.text===c?d:(l.edited.push({from:d.text,to:c}),{...d,text:c,at:i.toISOString()}))}),A=new Set(e);for(const n of t)a.has(n.id)||(A.has(n.id)?l.removed.push(n.text):m.push(n));return{rules:O(m,0),changes:l}}o(X,"applyRules");const T=o(t=>!!(t.added.length||t.edited.length||t.removed.length),"changed"),Y=o(()=>L(S()),"readHubRules");function q(t){const r=y.join(j.homedir(),".claude","kickflip","settings.json");let e=null;try{e=h.readFileSync(r,"utf8")}catch{}if(e!==null)try{JSON.parse(e)}catch{s(`${r} is not valid JSON; fix it before changing the hub rules`)}h.mkdirSync(y.dirname(r),{recursive:!0}),E({...S(),rules:t})}o(q,"writeHubRules");const k=o(t=>t.replace(/\s+/g," ").trim(),"flat"),z=o((t,r)=>[...t.map(e=>`hub ${k(e.text)}`),...r.map(e=>`board ${k(e.text)}`)],"ruleLines");export{b as RULES_MAX,w as RULE_BY,$ as RULE_MAX,D as RuleError,v as addRule,X as applyRules,T as changed,B as editRule,Y as readHubRules,H as removeRule,z as ruleLines,p as ruleText,L as rulesOf,q as writeHubRules};
@@ -0,0 +1 @@
1
+ var H=Object.defineProperty;var f=(e,t)=>H(e,"name",{value:t,configurable:!0});import{FOREVER as m,durationEnd as T,inRange as j}from"./duration.mjs";const d=Object.freeze({windows:Object.freeze([Object.freeze({from:"08:00",every_minutes:15}),Object.freeze({from:"18:00",every_minutes:30}),Object.freeze({from:"22:00",every_minutes:60})]),hold_overnight:!0,wakes:Object.freeze(["08:00","09:00"]),notifications:Object.freeze({on:!0,weekends_off:!1,snoozed_until:null})}),b="never",g=8,x=8,p=/^([01]\d|2[0-3]):([0-5]\d)$/,$=2*6e4,w=f(e=>{const[t,n]=e.split(":").map(Number);return t*60+n},"minutes"),W=f(e=>e.getHours()*60+e.getMinutes()+e.getSeconds()/60+e.getMilliseconds()/6e4,"minuteOfDay"),c=f((e,t,n=0)=>{const[o,r]=t.split(":").map(Number);return new Date(e.getFullYear(),e.getMonth(),e.getDate()+n,o,r)},"atOn"),_=f((e,t)=>{const n=c(e,t);return n>e?n:c(e,t,1)},"nextAt"),I=f((e,t)=>{const n=c(e,t);return n<=e?n:c(e,t,-1)},"lastAt"),M=f(e=>({windows:e.windows.map(t=>({from:t.from,every_minutes:t.every_minutes})),hold_overnight:e.hold_overnight,wakes:[...e.wakes],notifications:{...e.notifications}}),"copy");function S(e){const t=e?.on??d.notifications.on,n=e?.weekends_off??d.notifications.weekends_off,o=e?.snoozed_until??null;return typeof t!="boolean"?{error:"Notifications must be on or off."}:typeof n!="boolean"?{error:"Off on weekends must be on or off."}:o!==null&&o!==m&&(typeof o!="string"||Number.isNaN(Date.parse(o)))?{error:"Snooze must end at a date and time, or be forever."}:{notifications:{on:t,weekends_off:n,snoozed_until:o===null||o===m?o:new Date(o).toISOString()}}}f(S,"validateNotifications");function A(e){const t=e?.windows,n=e?.hold_overnight??d.hold_overnight,o=e?.wakes??d.wakes,{notifications:r,error:i}=S(e?.notifications);if(i)return{error:i};if(typeof n!="boolean")return{error:"Hold pushes overnight must be on or off."};if(!Array.isArray(o))return{error:"Wake times must be a list of HH:MM times."};if(o.length>x)return{error:`Use at most ${x} wake times.`};for(const[a,u]of o.entries()){if(typeof u!="string"||!p.test(u))return{error:`Wake ${a+1} must be HH:MM (24-hour).`};if(o.indexOf(u)!==a)return{error:`Two wakes are at ${u}; keep one.`}}if(!Array.isArray(t)||!t.length)return{error:"Add at least one window."};if(t.length>g)return{error:`Use at most ${g} windows.`};const s=[];for(const[a,u]of t.entries()){if(typeof u?.from!="string"||!p.test(u.from))return{error:`Window ${a+1}'s start must be HH:MM (24-hour).`};const h=Number(u.every_minutes);if(u.every_minutes===""||u.every_minutes==null||!Number.isInteger(h)||h<5||h>120)return{error:`Window ${a+1}'s interval must be a whole number of minutes from 5 to 120.`};if(s.some(y=>y.from===u.from))return{error:`Two windows start at ${u.from}; give each its own start.`};s.push({from:u.from,every_minutes:h})}return{settings:{windows:s.sort((a,u)=>w(a.from)-w(u.from)),hold_overnight:n,wakes:[...o].sort((a,u)=>w(a)-w(u)),notifications:r}}}f(A,"validate");function P(e){const t=e?.immediate?.start,n=e?.immediate?.end,o=e?.bundle?.until;if(![t,n,o].every(a=>typeof a=="string"&&p.test(a)))return null;const r=Number(e.bundle.every_minutes),i=[{from:t,every_minutes:d.windows[0].every_minutes}];n!==t&&o!==n&&i.push({from:n,every_minutes:Number.isInteger(r)?r:d.windows[1].every_minutes});const s=o===n?n:o;return s!==t&&!i.some(a=>a.from===s)&&i.push({from:s,every_minutes:d.windows[2].every_minutes}),A({windows:i,hold_overnight:o!==t}).settings||null}f(P,"fromPush");function G(e){const t=S(e?.notifications).notifications||M(d).notifications,n=f(o=>o&&{...o,notifications:t},"withThem");return A({...e?.polling,notifications:t}).settings||n(P(e?.push))||n(M(d))}f(G,"pollingOf");function v(e,t){const n=t.windows,o=W(e);let r=n.length-1;for(let a=0;a<n.length;a++)w(n[a].from)<=o&&(r=a);const i=n[r],s=n[(r+1)%n.length];return{from:i.from,every_minutes:i.every_minutes,startedAt:I(e,i.from),endsAt:_(e,s.from)}}f(v,"windowAt");function k(e,t){const n=e.windows;if(n.length===1)return 1440;const o=w(n[(t+1)%n.length].from)-w(n[t].from);return o>0?o:o+1440}f(k,"windowSpan");const R=f((e,t)=>Math.ceil(k(e,t)/e.windows[t].every_minutes),"checksIn"),q=f(e=>e.windows.reduce((t,n,o)=>t+R(e,o),0),"checksADay"),z=f(e=>e.hold_overnight&&e.windows.length>1,"holdsOvernight");function B(e){const t=e.windows.length-1;return e.windows.flatMap((n,o)=>{const r=w(n.from),i=r+k(e,o),s=z(e)&&o===t;return e.windows.length===1?[{window:o,start:0,end:1440,held:s}]:i<=1440?[{window:o,start:r,end:i,held:s}]:[{window:o,start:r,end:1440,held:s},{window:o,start:0,end:i-1440,held:s}]})}f(B,"daySegments");function F(e,t){if(e.windows.length>=g)return null;const n=w(e.windows[t].from),o=k(e,t),r=Math.round((n+o/2)/15)*15;if(r<=n||r>=n+o)return null;const i=r%1440;return`${String(Math.floor(i/60)).padStart(2,"0")}:${String(i%60).padStart(2,"0")}`}f(F,"splitPoint");function J(e,{op:t,w:n,value:o}){const r=e.windows.map(u=>({from:u.from,every_minutes:u.every_minutes})),i={windows:r,hold_overnight:e.hold_overnight,wakes:[...e.wakes]},s=r.findIndex(u=>u.from===n);if(["from","every","split","remove"].includes(t)&&s<0)return{error:"That window has changed. Pick it again on the bar."};const a=s<0?e.windows[0].from:n;switch(t){case"from":return r[s].from=o,{input:i,select:o};case"every":return r[s].every_minutes=o,{input:i,select:a};case"split":{const u=F(e,s);return u?(r.push({from:u,every_minutes:r[s].every_minutes}),{input:i,select:u}):{error:r.length>=g?`Use at most ${g} windows.`:"This window is too short to split."}}case"remove":{if(r.length<2)return{error:"Keep at least one window."};const u=r[(s-1+r.length)%r.length].from;return r.splice(s,1),{input:i,select:u}}case"hold":return i.hold_overnight=o==="on",{input:i,select:a};case"wake-add":return i.wakes.push(o),{input:i,select:a};case"wake-remove":return i.wakes=i.wakes.filter(u=>u!==o),{input:i,select:a};default:return{error:"That change is not one the Polling modal makes."}}}f(J,"editPolling");function U(e,t){if(!z(t))return!1;const n=v(e,t);return n.from===t.windows.at(-1).from&&e-n.startedAt>$}f(U,"holdsPushes");function D(e,t){if(!t.notifications?.weekends_off)return null;const[n,o]=[t.windows[0].from,t.windows.at(-1).from];let r=c(e,o,-((e.getDay()-5+7)%7));r>e&&(r=c(r,o,-7));const i=c(r,n,3);return e-r>$&&e<i?i:null}f(D,"weekendEnd");function L(e,t){if(t.notifications?.on===!1)return{until:b,why:"off"};if(t.notifications?.snoozed_until===m)return{until:m,why:"snoozed"};const n=t.notifications?.snoozed_until?new Date(t.notifications.snoozed_until):null,o=f(s=>(n>s?["snoozed",n]:null)||D(s,t)&&["weekend",D(s,t)]||U(s,t)&&["overnight",v(s,t).endsAt],"at"),r=o(e);if(!r)return null;let i=r[1];for(let s=o(i),a=0;s&&a<8;s=o(i),a++)i=s[1];return{until:i,why:r[0]}}f(L,"hold");function K(e,t){return L(e,t)?.until??null}f(K,"heldUntil");function Q(e,t){let n=O(e,t);for(let o=0;o<8;o++){const r=K(n,t);if(r===b||r===m)return r;if(!r)return n;n=O(new Date(r.getTime()-1),t)}return n}f(Q,"flushAt");function Z(e,t,n){const o=String(t),[r,i]=[n.windows[0].from,n.windows.length];let s;if(o==="morning")s=_(e,r);else if(o==="tonight")s=i>1?_(e,n.windows[Math.max(1,i-2)].from):null;else if(o==="monday")s=c(e,r,(8-e.getDay())%7),s<=e&&(s=c(s,r,7));else return T(e,o);return j(e,s)?s:null}f(Z,"snoozeEnd");function ee(e,t=new Date){if(e.why==="off")return"notifications are off";if(e.until===m)return"snoozed until you end it";const n=E(e.until,t);return e.why==="snoozed"?`snoozed until ${n}`:e.why==="weekend"?`held for the weekend until ${n}`:`held overnight until ${n}`}f(ee,"holdLine");function O(e,t){const n=v(e,t),o=n.every_minutes*6e4,r=new Date(n.startedAt.getTime()+(Math.floor((e-n.startedAt)/o)+1)*o);return r<n.endsAt?r:n.endsAt}f(O,"nextSlot");function V(e){return`${e.getFullYear()}-${String(e.getMonth()+1).padStart(2,"0")}-${String(e.getDate()).padStart(2,"0")}`}f(V,"localDay");function te(e,t,n={}){const o=V(e);return t.wakes.filter(r=>c(e,r)<=e&&n?.[r]!==o)}f(te,"wakesDue");const X=f(e=>/^\/([^/?#]+)(?:[/?#]|$)/.exec(e||"")?.[1]??null,"routeOf");function ne(e,t={}){const n=new Map;e.forEach((r,i)=>{r.tag&&n.set(r.tag,i)});const o=new Map;return e.filter((r,i)=>!r.tag||n.get(r.tag)===i).forEach(r=>{const i=X(r.url)??Symbol(r.id);o.set(i,[...o.get(i)||[],r])}),[...o].flatMap(([r,i])=>{if(i.length===1){const[l]=i;return[{title:l.title,body:l.body||"",url:l.url||"/",tag:l.tag||void 0,kind:l.kind,members:[l.id]}]}const s=t[r];if(s&&!s.count)return[];const a=r==="_hub"?"Hub":r,u=i.map(l=>l.title),h=s?`${s.count} need${s.count===1?"s":""} you on ${a}`:`${i.length} updates on ${a}`,y=s?s.summary:[...u.slice(0,2),...u.length>2?[`and ${u.length-2} more`]:[]].join(" \xB7 "),N=i.every(l=>l.kind==="action")?"action":i.find(l=>l.kind!=="action").kind;return[{title:h,body:y,url:`/${r}/`,tag:`summary:${r}`,kind:N,members:i.map(l=>l.id)}]})}f(ne,"notifications");const oe=f((e,t=new Date)=>e===m?"once the snooze ends":E(e,t),"sendWords");function E(e,t=new Date){const n=e.getHours(),o=`${n%12||12}:${String(e.getMinutes()).padStart(2,"0")}${n<12?"am":"pm"}`,r=Math.round((c(e,"00:00")-c(t,"00:00"))/864e5);return r===0?o:r===1?`${o} tomorrow`:`${o} ${e.toDateString().slice(0,10)}`}f(E,"clock");export{d as DEFAULTS,b as NEVER,q as checksADay,R as checksIn,E as clock,B as daySegments,J as editPolling,Q as flushAt,P as fromPush,K as heldUntil,L as hold,ee as holdLine,z as holdsOvernight,U as holdsPushes,V as localDay,O as nextSlot,ne as notifications,G as pollingOf,oe as sendWords,Z as snoozeEnd,F as splitPoint,A as validate,te as wakesDue,v as windowAt,k as windowSpan};
@@ -0,0 +1,2 @@
1
+ var z=Object.defineProperty;var a=(e,s)=>z(e,"name",{value:s,configurable:!0});import x from"node:fs";import h from"node:path";import A from"node:os";import{spawn as B,spawnSync as V}from"node:child_process";import{fileURLToPath as Z}from"node:url";import{joinName as ee,routeRecord as m,validRoute as se}from"./routes.mjs";import{withFileLock as te}from"./locks.mjs";import{RESUME_STATE as R,carryCursor as oe,hubBoard as ne,isListening as re,listBoards as M,oldestUnheard as ie,readManifest as y,readJSON as ae,saveJSON as ce,watchedCounts as de,watchedLines as ue,watcherCursor as le,withManifestLock as fe,writeManifest as pe}from"./boards.mjs";import{readUsageGate as me,heldUsageEvent as he}from"../shared/usage-gate.mjs";import{liveSessions as S,sessionRecords as we,processStarts as be,verifiedProcess as ge}from"./live.mjs";import{deliver as $e}from"./push.mjs";import{WAIT_MS as ke}from"./prompts.mjs";import{nodeBin as ye,storedPath as G}from"../bin/install.mjs";import{reposDir as Ie}from"../shared/repos.mjs";import{isClone as Se}from"./repos.mjs";import{trustFolder as Te}from"./trust.mjs";const C=G(h.resolve(h.dirname(Z(import.meta.url)),"..")),_e=3*6e4,xe=5*6e4,ve=15*6e4,Oe=a((e,s=Date.now())=>!!e.error||s-Date.parse(e.asked_at||e.since)>=ve,"startFailed");function is(e,s=Date.now()){return Object.entries(e.starting||{}).filter(([t])=>!m(e.sites,t)).map(([t,o])=>({route:t,title:o.title||o.label||t,repo:o.repo,...o.phase?{phase:o.phase}:{},...o.branch?{branch:o.branch,tickets:o.tickets}:{},askedAt:o.asked_at||o.since,failed:Oe(o,s),...o.error?{error:o.error}:{}}))}a(is,"startingBoards");const Ee=ke/1e3+60,je=JSON.stringify({worktree:{bgIsolation:"none"},hooks:{PermissionRequest:[{matcher:"*",hooks:[{type:"command",command:`"${ye()}" "${h.join(C,"hub","permission-hook.mjs")}"`,timeout:Ee}]}]}});class p extends Error{static{a(this,"SessionError")}constructor(s,t){super(s),this.reason=t}}function Ae(e,s){if(!se(e))throw new p("Choose a board route.","none");return te(h.join(A.homedir(),".claude","kickflip","session-locks",e),()=>s({resume:a(t=>Ge(e,t),"resume")}),{staleAfter:12e4,timeout:9e4})}a(Ae,"withSessionLock");function v(){if(process.env.KICKFLIP_CLAUDE)return process.env.KICKFLIP_CLAUDE;const e=h.join(A.homedir(),".local","bin","claude");return x.existsSync(e)?e:"claude"}a(v,"claudeBin");const O=a(e=>e==="CLAUDECODE"||e==="CLAUDE_PID"||e==="CLAUDE_EFFORT"||e.startsWith("CLAUDE_CODE_"),"inherited");function F(){return process.env.KICKFLIP_TMUX?process.env.KICKFLIP_TMUX:["/opt/homebrew/bin/tmux","/usr/local/bin/tmux","/usr/bin/tmux"].find(e=>x.existsSync(e))||"tmux"}a(F,"tmuxBin");function K(e){const s=V(F(),e,{encoding:"utf8",timeout:1e4,env:Object.fromEntries(Object.entries(process.env).filter(([t])=>!O(t)))});return s.error?.code==="ENOENT"?{status:127,stdout:"",stderr:"tmux isn't installed; kickflip doctor names how to install it"}:{status:s.status??1,stdout:s.stdout||"",stderr:s.stderr||s.error?.message||""}}a(K,"runTmux");const T=a(e=>`kickflip-${e}`,"tmuxName"),J=a((e,s)=>e(["has-session","-t",`=${T(s)}`]).status===0,"hasTmux");function L(e,{cwd:s,timeout:t=6e4}){const o=v(),i=Object.fromEntries(Object.entries(process.env).filter(([r])=>!O(r)));return i.CLAUDE_BG_ISOLATION="none",h.isAbsolute(o)&&(i.PATH=`${h.dirname(o)}:${i.PATH||""}`),new Promise(r=>{let n="",f="",d;try{d=B(o,e,{cwd:s,env:i,detached:!0,stdio:["ignore","pipe","pipe"]})}catch(l){r({status:1,stdout:n,stderr:l.message});return}const c=a(l=>{clearTimeout(w),d.stdout.destroy(),d.stderr.destroy(),r({status:l,stdout:n,stderr:f})},"done"),w=setTimeout(()=>{try{d.kill("SIGKILL")}catch{}c(1)},t);d.stdout.on("data",l=>{n+=l}),d.stderr.on("data",l=>{f+=l}),d.on("error",l=>{f+=l.message,c(1)}),d.on("close",l=>c(l??1)),d.on("exit",l=>setTimeout(()=>c(l??1),300)),d.unref()})}a(L,"runClaude");async function Ce(e,{cwd:s,run:t=L,starts:o=be,signal:i=process.kill}={}){if(e.kind==="bg"){const r=await t(["stop",e.jobId||e.sessionId.slice(0,8)],{cwd:s,timeout:8e3});if(r.status!==0)throw new p(`claude stop failed: ${(r.stderr||r.stdout).trim().slice(0,300)}`,"failed")}else{if(!ge(e,o))throw new p(`Cannot verify session ${e.sessionId}'s process identity; refusing to signal pid ${e.pid}.`,"failed");i(e.pid,"SIGTERM")}}a(Ce,"runSessionStop");async function as(e,{live:s=S,stop:t=Ce}={}){if(!e?.session)return;const o=s().get(e.session);if(!o)return;await t(o,{cwd:e.repo||o.cwd||A.homedir()});const i=Date.now()+5e3;for(;s().get(e.session)?.pid===o.pid;){if(Date.now()>=i)throw new p(`session ${e.session} did not stop`,"failed");await new Promise(r=>setTimeout(r,100))}}a(as,"stopSession");function Le(e){const s=`=${T(e)}`;return De(F(),process.env.TMUX?["switch-client","-t",s]:["attach-session","-t",s])}a(Le,"attachTmux");function De(e,s){const t=Object.fromEntries(Object.entries(process.env).filter(([r])=>!O(r)&&r!=="CLAUDE_BG_ISOLATION")),o=a(()=>{},"ignore");process.on("SIGINT",o).on("SIGQUIT",o);const i=a(()=>process.off("SIGINT",o).off("SIGQUIT",o),"settle");return new Promise((r,n)=>{const f=B(e,s,{env:t,stdio:"inherit"});f.on("error",d=>{i(),n(new p(d.message,"failed"))}),f.on("close",d=>{i(),r({status:d??1})})})}a(De,"runForeground");const D=a(({name:e,model:s})=>["--name",e,"--permission-mode","auto","--settings",je,...s?["--model",s]:[]],"common"),Ue=a(e=>`'${e.replaceAll("'","'\\''")}'`,"quote"),E=a((e,s)=>`--name ${Ue(ee(e,s))}`,"boardFlag"),Pe=a(({route:e,name:s,model:t,prompt:o})=>["--bg",...D({name:s,model:t}),`/kickflip ${E(e,s)}${o?` ${o}`:""}`],"startArgv"),Ne=a((e,s,t)=>`The hub resumed this session because /${e} had nobody listening. Rejoin the board as session ${s} (kickflip steps 3 and 5): rerun \`node ${h.join(C,"bin","kickflip.mjs")} join ${E(e,t)} --session ${s}\`, relaunch its watcher as a background task, then go on with what its events ask. If your session id (the folder above your scratchpad) isn't ${s}, rejoin with yours and --force.`,"resumePrompt"),Be=a(({id:e,route:s,name:t,model:o,background:i})=>["--resume",e,"--bg",...i?[]:D({name:t,model:o}),Ne(s,e,t)],"resumeArgv"),W=a((e,s)=>`You are the hub session. Read \`${h.join(C,"docs","hub-session.md")}\`, then rejoin /${e} (join ${E(e,s)}) and relaunch its watcher.`,"hubPrompt"),H=a(({route:e,title:s,id:t,model:o})=>[...t?["--resume",t]:[],...D({name:s,model:o}),t?`${W(e,s)} If your session id (the folder above your scratchpad) isn't ${t}, rejoin with yours and --force.`:`/kickflip ${E(e,s)} --hub
2
+ ${W(e,s)}`],"hubArgv");function X(e,s,{cwd:t,tmux:o}){const i=v(),r=[...new Set(["CLAUDECODE","CLAUDE_BG_ISOLATION",...Object.keys(process.env).filter(O)])].flatMap(f=>["-u",f]),n=o(["new-session","-d","-s",T(e),"-c",t,"/usr/bin/env",...r,...h.isAbsolute(i)?[`PATH=${h.dirname(i)}:${process.env.PATH||""}`]:[],i,...s]);if(n.status!==0)throw new p(`tmux new-session failed (${n.status}): ${(n.stderr||n.stdout).trim().slice(0,300)}`,"failed");return[i,...s]}a(X,"startTmux");async function cs({manifest:e=y(),repo:s,tmux:t=K,attach:o=Le,live:i=S}={}){const r=ne(e);if(!r)throw new p("No hub board is flagged; flag one with kickflip set <route> --hub, or run kickflip join --hub in its repo folder","hub");if(!J(t,r.route)){const n=r.owner?.session?r.owner:null;if(n&&!n.repo&&!s)throw new p(`/${r.route}'s session ${n.session} has no folder recorded; pass --repo <its folder>`,"none");P(n,i);const f=n?.repo||h.resolve(s||process.cwd());if(!x.statSync(f,{throwIfNoEntry:!1})?.isDirectory())throw new p(n?.repo?`/${r.route}'s recorded folder ${f} is gone; rejoin from its new folder with --repo`:`no folder at ${f}`,"none");X(r.route,H({route:r.route,title:r.title,id:n?.session,model:n?.model}),{cwd:f,tmux:t})}return o(r.route)}a(cs,"hubSession");function U(e,s){const t=m(e?.sites,s),o=t?.owner?.session?t.owner:m(e?.starting,s);return o?.session?{...o,title:t?.title||s}:null}a(U,"sessionOf");async function q(e,s=null){const t=e.match(/backgrounded\s*·\s*([0-9a-f]{8})\b/)?.[1]||e.match(/\bclaude attach ([0-9a-f]{8})\b/)?.[1];if(!t)return{session:s,confirmed:!1};if(s?.startsWith(t))return{session:s,confirmed:!0};for(let o=0;o<20;o++){const i=we().find(r=>r.jobId===t||r.sessionId.startsWith(t));if(i)return{session:i.sessionId,confirmed:!0};await new Promise(r=>setTimeout(r,500))}return{session:s,confirmed:!1}}a(q,"startedId");async function Re(e,{run:s,cwd:t}){const o=await s(["agents","--json","--all"],{cwd:t});try{return JSON.parse(o.stdout).some(i=>i?.sessionId===e&&i.kind==="background")}catch{return!1}}a(Re,"isBackground");function P(e,s){const t=e&&s().get(e.session);if(t)throw new p(`session ${e.session} is live (${t.kind==="bg"?"in the background":"in a terminal"}, pid ${t.pid}); it gets no second process`,"live")}a(P,"refuseLive");async function Q(e,{from:s,to:t,mark:o}){await fe(async()=>{const i=y(),r=M(i).find(f=>f.route===e);if(r?.owner?.session===s&&(o||t!==s)&&oe(r,{from:s,to:t,mark:o}),t===s)return;const n=m(i.sites,e);if(n?.owner?.session===s)Object.assign(n,{owner:{...n.owner,session:t},updated:new Date().toISOString()});else if(m(i.starting,e)?.session===s)i.starting[e]={...m(i.starting,e),session:t};else return;await pe(i)})}a(Q,"moveOwner");async function ds(e,{manifest:s=y(),repo:t,model:o,prompt:i,run:r=L,live:n=S,clones:f=Ie()}={}){if(m(s.closed,e))throw new p(`/${e} is closed; use kickflip reopen ${e}`,"none");const d=U(s,e);P(d,n);const c=h.resolve(t||d?.repo||process.cwd());if(!x.statSync(c,{throwIfNoEntry:!1})?.isDirectory())throw new p(`no folder at ${c}`,"none");if(Se(c,f))try{Te(c)}catch(g){throw new p(`Couldn't trust this folder for Claude Code: ${g.message}`,"failed")}const w=m(s?.sites,e)?.title||m(s?.starting,e)?.title||e,l=Pe({route:e,name:w,model:o||d?.model,prompt:i}),u=await r(l,{cwd:c});if(u.status!==0){const g=(u.stderr||"").trim()||(u.stdout||"").trim();throw new p(g?`claude --bg failed (${u.status}): ${g.slice(0,300)}`:`claude exited ${u.status} with no output; check \`claude agents\``,"failed")}const{session:$,confirmed:I}=await q(u.stdout);if(!$)throw new p(`claude --bg started a session but no session record names it yet; find it with claude agents (${u.stdout.trim().slice(0,300)})`,"failed");return{route:e,session:$,confirmed:I,repo:G(c),model:o||d?.model||null,name:w,argv:[v(),...l],output:u.stdout.trim()}}a(ds,"startSession");function Me(e,s={}){return Ae(e,({resume:t})=>t(s))}a(Me,"resumeSession");async function Ge(e,{manifest:s=y(),run:t=L,tmux:o=K,live:i=S,mark:r=null}={}){if(m(y().closed,e))throw new p(`/${e} is closed; use kickflip reopen ${e}`,"none");const n=U(s,e);if(!n)throw new p(`/${e} has no recorded session`,"none");if(!n.repo)throw new p(`/${e}'s session ${n.session} has no folder recorded; its session records one by rejoining (kickflip join, --repo)`,"none");if(P(n,i),m(s.sites,e)?.hub===!0){if(J(o,e))throw new p(`/${e}'s tmux session ${T(e)} is still open; attach with kickflip hub`,"live");const u=X(e,H({route:e,title:n.title,id:n.session,model:n.model}),{cwd:n.repo,tmux:o});return await Q(e,{from:n.session,to:n.session,mark:r}),{route:e,session:n.session,previous:n.session,confirmed:!1,repo:n.repo,argv:u,output:`in tmux session ${T(e)}`}}const f=await Re(n.session,{run:t,cwd:n.repo}),d=Be({id:n.session,route:e,name:n.title,model:n.model,background:f}),c=await t(d,{cwd:n.repo});if(c.status!==0)throw new p(`claude --resume failed (${c.status}): ${(c.stderr||c.stdout).trim().slice(0,300)}`,"failed");const{session:w,confirmed:l}=await q(c.stdout,n.session);return await Q(e,{from:n.session,to:w,mark:r}),{route:e,session:w,previous:n.session,confirmed:l,repo:n.repo,argv:[v(),...d],output:c.stdout.trim()}}a(Ge,"resumeRecordedSession");function Fe(e,{listening:s,live:t,recorded:o,counts:i,waiting:r,asked:n=!1,now:f}){if(s)return{action:"none",entry:null};const d=new Date(f).toISOString(),c=e?{...e}:{down_since:d};return t?delete c.mark:!c.mark&&i&&(c.mark=i),c.pushed_at?{action:"none",entry:c}:c.resumed_at?c.error||f-Date.parse(c.resumed_at)>=xe?{action:"push",entry:{...c,pushed_at:d}}:{action:"none",entry:c}:!o||t?{action:"none",entry:c}:!!(e?.mark&&i&&Object.entries(i).some(([l,u])=>u>(m(e.mark,l)??0)))||n||r&&f-Date.parse(c.down_since)>=_e?{action:"resume",entry:{...c,resumed_at:d}}:{action:"none",entry:c}}a(Fe,"resumeStep");async function us({manifest:e=y(),now:s=new Date,live:t=S,asked:o=new Set,listening:i=a((d,c)=>re(d,{now:s.getTime(),running:c}),"listening"),resume:r=a((d,c)=>Me(d,{manifest:e,live:t,mark:c}),"resume"),push:n=$e,gate:f=me({now:s})}={}){const d=ae(R,{}),c=t(),w=Object.create(null),l={resumed:[],pushed:[],failed:[]};for(const u of M(e)){const $=U(e,u.route),I=!!($&&c.has($.session)),g=i(u,c);let{action:_,entry:b}=Fe(m(d,u.route),{listening:g,live:I,recorded:!!$?.repo,now:s.getTime(),asked:o.has(u.route),counts:g?null:de(u),waiting:!g&&!I&&ie(u)!==null});if(_==="resume"&&f.state==="paused"){const k=m(d,u.route)?.mark||le(u);Object.entries(ue(u)).flatMap(([j,Y])=>Y.slice(m(k,j)||0)).some(j=>!he(j))||(_="none",delete b.resumed_at)}if(_==="resume")try{await r(u.route,b.mark),l.resumed.push(u.route)}catch(k){b={...b,error:k.message},l.failed.push(`${u.route}: could not resume its session (${k.message})`)}else if(_==="push"){const k=b.error?`The hub couldn't resume it: ${b.error}`:I?"Its session is running but hasn't relaunched its watcher.":`It still isn't listening after the hub resumed it. Try kickflip resume ${u.route}, or claude agents.`;try{await n({title:`${u.title}'s session didn't come back`,body:k,url:`/${u.route}/`,tag:`resume-${u.route}`},{kind:"fault",immediate:!0,at:s}),l.pushed.push(u.route)}catch(N){b={...b,pushed_at:void 0},l.failed.push(`${u.route}: could not push that its session didn't come back (${N.message})`)}}b&&(w[u.route]=b)}return JSON.stringify(w)!==JSON.stringify(d)&&ce(R,w),l}a(us,"checkSessions");export{je as BG_SETTINGS,xe as COMEBACK_WAIT,Ee as PROMPT_HOOK_TIMEOUT_S,_e as RESUME_AFTER,ve as STARTING_WAIT,p as SessionError,Le as attachTmux,us as checkSessions,v as claudeBin,H as hubArgv,cs as hubSession,Be as resumeArgv,Ne as resumePrompt,Me as resumeSession,Fe as resumeStep,L as runClaude,Ce as runSessionStop,K as runTmux,U as sessionOf,Pe as startArgv,Oe as startFailed,ds as startSession,is as startingBoards,as as stopSession,F as tmuxBin,T as tmuxName,Ae as withSessionLock};
package/hub/shell.mjs ADDED
@@ -0,0 +1,2 @@
1
+ var d=Object.defineProperty;var s=(t,o)=>d(t,"name",{value:o,configurable:!0});import _ from"node:crypto";import a from"node:fs";import l from"node:path";import{fileURLToPath as g}from"node:url";const f=l.dirname(g(import.meta.url)),b=l.join(f,"..","board","public"),S=[b,l.join(f,"..","starter","public")],x=l.join(f,"..","shared","colors.css"),U=new Set(["","settings.html","questions.html","notes.html"]),w=s(t=>t===""||t.endsWith("/")||/\.html?$/.test(t),"isPagePath"),$=new Set(["/","/_hub/","/_hub/settings","/_hub/settings/polling"]),G=s(t=>t["x-app-shell"]==="1"?"document":t["sec-fetch-dest"],"destOf");function H({method:t="GET",pathname:o="/",search:n="",dest:e,sites:c=new Set}){if($.has(o))return t==="GET"||t==="HEAD"?"shell":"hub";if(o==="/_hub"||o.startsWith("/_hub/")||o==="/sw.js"||o==="/manifest.webmanifest"||o==="/favicon.ico")return"hub";if(t!=="GET"||e!=="document"||new URLSearchParams(n).has("alone"))return"forward";const r=/^\/([^/]+)\/(.*)$/.exec(o);let i=null;try{i=r?decodeURIComponent(r[1]):null}catch{i=null}return i!==null&&c.has(i)&&w(r[2])?"shell":"forward"}s(H,"pageOf");function j(t,o){return t.replace(/\b(src|href)="\/_hub\/app\/([\w.-]+\.(?:js|css))"/g,(n,e,c)=>{const r=o(c);return r?`${e}="/_hub/app/${c}?v=${r}"`:n})}s(j,"versionAppAssets");function P(t){if(!/^[\w.-]+\.(?:js|css|png)$/.test(t))return null;for(const o of S){const n=l.join(o,t);try{if(a.statSync(n).isFile())return n}catch{}}return null}s(P,"appFile");const m=s(t=>{try{const o=a.statSync(t);return`${Math.floor(o.mtimeMs).toString(36)}-${o.size.toString(36)}`}catch{return null}},"fileVersion"),y=s(()=>m(x)||"0","colorsVersion");function I({sidebar:t="",sidebarData:o="null",touchIcon:n="/_hub/icon-180.png"}={}){const e=[],c=j(a.readFileSync(l.join(b,"app.html"),"utf8"),p=>{const u=P(p),h=u&&m(u);return e.push(`${p} ${h}`),h}),r=_.createHash("sha1").update(e.join(`
2
+ `)).digest("base64url").slice(0,16),i={sidebar:t,sidebarData:o,touchIcon:n,colorsVersion:y(),build:r};return c.replace(/\{\{(sidebar|sidebarData|touchIcon|colorsVersion|build)\}\}/g,(p,u)=>i[u])}s(I,"shellHtml");export{b as BOARD_PUBLIC,U as VIEW_PAGES,P as appFile,y as colorsVersion,G as destOf,m as fileVersion,H as pageOf,I as shellHtml,j as versionAppAssets};
package/hub/slack.mjs ADDED
@@ -0,0 +1,2 @@
1
+ var w=Object.defineProperty;var n=(r,t)=>w(r,"name",{value:t,configurable:!0});import o from"node:fs";import s from"node:path";import m from"node:os";import{withFileLock as h}from"./locks.mjs";const i=["self_dm","owner_slack_user","slack_domain","timezone"],a=n(()=>s.join(m.homedir(),".claude","kickflip","slack.json"),"fileOf"),c=n(r=>{try{return JSON.parse(o.readFileSync(r,"utf8"))||{}}catch{return{}}},"read"),d=n(r=>Object.fromEntries(i.map(t=>[t,typeof r?.[t]=="string"?r[t]:""])),"identityOf");function f(r){const t=a();o.mkdirSync(s.dirname(t),{recursive:!0});const e=`${t}.${process.pid}.tmp`;return o.writeFileSync(e,JSON.stringify(r,null,2)+`
2
+ `,{mode:384}),o.chmodSync(e,384),o.renameSync(e,t),r}n(f,"save");function l(){const r=d(c(a()));if(i.some(e=>r[e]))return r;const t=d(c(s.join(m.homedir(),".claude","slack-bridge","state.json")).config);return i.some(e=>t[e])?f(t):r}n(l,"readSlack");async function $(r){for(const[t,e]of Object.entries(r)){if(!i.includes(t)||typeof e!="string")throw new Error(`Unknown Slack setting ${t}`);if(t==="self_dm"&&e&&!/^D[A-Z0-9]+$/.test(e))throw new Error("self-dm must be a DM channel id");if(t==="owner_slack_user"&&e&&!/^U[A-Z0-9]+$/.test(e))throw new Error("user must be a Slack user id");if(t==="slack_domain"&&e&&!/^[a-z0-9][a-z0-9-]*$/.test(e))throw new Error("domain must be a Slack workspace domain");if(t==="timezone"&&e)try{new Intl.DateTimeFormat("en",{timeZone:e})}catch{throw new Error("timezone must be an IANA timezone such as Area/City")}}return h(a(),()=>f({...l(),...r}))}n($,"setSlack");function z(r=new Date){const t=l().timezone||Intl.DateTimeFormat().resolvedOptions().timeZone,e=Object.fromEntries(new Intl.DateTimeFormat("en-US",{timeZone:t,year:"numeric",month:"2-digit",day:"2-digit",weekday:"short",hour:"2-digit",hourCycle:"h23"}).formatToParts(r).map(({type:u,value:p})=>[u,p]));return{date:`${e.year}-${e.month}-${e.day}`,morning:!["Sat","Sun"].includes(e.weekday)&&Number(e.hour)>=9}}n(z,"slackDay");export{l as readSlack,$ as setSlack,z as slackDay};
package/hub/stream.mjs ADDED
@@ -0,0 +1,19 @@
1
+ var F=Object.defineProperty;var r=(w,g)=>F(w,"name",{value:g,configurable:!0});import W from"node:http";import A from"node:fs";import B from"node:path";function P({targets:w,watched:g=r(()=>[],"watched"),sidebar:I,retryMs:O=2e3,sidebarMs:j=15e3,pingMs:H=25e3,settleMs:M=300,log:v=r(()=>{},"log")}){const o=new Set,a=new Map,c=new Map,S=new Set;let l=null,y=[],u=new Set,d=null;const h=r((t,e=o)=>{for(const n of e)n.write(t)},"write"),T=r((t,e)=>`event: ${t}
2
+ data: ${JSON.stringify(e)}
3
+
4
+ `,"event");async function m(t=[]){let e;try{e=await I({fresh:new Set(t)})}catch(i){v(`sidebar for the stream failed: ${i.message}`);return}const n=JSON.stringify(e);n!==l&&(l=n,h(`event: sidebar
5
+ data: ${n}
6
+
7
+ `))}r(m,"refreshSidebar");function $(t){t&&u.add(t),!d&&(d=setTimeout(()=>{d=null;const e=[...u];u=new Set,m(e)},M))}r($,"sidebarSoon");function b(t){h(T("change",{route:t})),$(t)}r(b,"changed");function _(t,e){const n={port:e,req:null};a.set(t,n);const i=r(()=>{a.get(t)===n&&a.delete(t),n.req?.destroy()},"drop");n.req=W.get({host:"127.0.0.1",port:e,path:"/api/stream",headers:{accept:"text/event-stream"}},s=>{if(s.statusCode!==200){s.resume(),i();return}S.has(t)&&b(t),S.add(t),s.setEncoding("utf8");let f="";s.on("data",C=>{f+=C.replace(/\r\n/g,`
8
+ `);for(let p=f.indexOf(`
9
+
10
+ `);p>=0;p=f.indexOf(`
11
+
12
+ `)){const E=f.slice(0,p);f=f.slice(p+2),/^event: ?change$/m.test(E)&&b(t)}}),s.on("close",i),s.on("error",i)}),n.req.on("error",i)}r(_,"connect");function q(){let t;try{t=new Map(w().map(e=>[e.route,e.port]))}catch(e){v(`stream targets failed: ${e.message}`);return}for(const[e,n]of a)t.get(e)!==n.port&&(a.delete(e),n.req?.destroy());for(const[e,n]of t)a.has(e)||_(e,n)}r(q,"reconcile");function x(){let t;try{t=new Set(g().map(e=>e.dir))}catch(e){v(`stream watched failed: ${e.message}`);return}for(const[e,n]of c)t.has(e)||(n.close(),c.delete(e));for(const e of t)if(!c.has(e))try{const n=A.watch(B.join(e,"data"),(i,s)=>{s==="watcher.json"&&$()});n.on("error",()=>{n.close(),c.delete(e)}),c.set(e,n)}catch{}}r(x,"watchWatchers");function k(){q(),x(),y=[setInterval(()=>{q(),x()},O),setInterval(()=>m(),j),setInterval(()=>h(`: ping
13
+
14
+ `),H)]}r(k,"startHolding");function z(){for(const t of y)clearInterval(t);y=[],clearTimeout(d),d=null,u=new Set;for(const t of a.values())t.req?.destroy();a.clear();for(const t of c.values())t.close();c.clear(),S.clear(),l=null}r(z,"stopHolding");function J(t,e){e.writeHead(200,{"content-type":"text/event-stream; charset=utf-8","cache-control":"no-store",connection:"keep-alive"}),e.write(`retry: 3000
15
+
16
+ `),o.add(e),o.size===1&&k(),l?h(`event: sidebar
17
+ data: ${l}
18
+
19
+ `,[e]):m(),t.on("close",()=>{o.delete(e),o.size||z()})}r(J,"handle");function N(){for(const t of o)t.end();o.clear(),z()}return r(N,"close"),{handle:J,close:N,changed:r(t=>{o.size&&b(t)},"changed"),refresh:r(()=>{o.size&&m()},"refresh")}}r(P,"hubStream");export{P as hubStream};