@lovek4d/kickflip 0.1.0 → 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 (96) hide show
  1. package/CONTEXT.md +177 -78
  2. package/README.md +46 -5
  3. package/SKILL.md +58 -37
  4. package/bin/doctor.mjs +4 -3
  5. package/bin/install.mjs +3 -3
  6. package/bin/kickflip.mjs +87 -32
  7. package/bin/statusline.mjs +4 -3
  8. package/bin/wait-events.mjs +2 -2
  9. package/board/arch-review.mjs +5 -0
  10. package/board/bin/agent.mjs +1 -1
  11. package/board/bin/board.mjs +241 -69
  12. package/board/bin/builder-stop-hook.mjs +4 -3
  13. package/board/domain.mjs +5 -3
  14. package/board/on-host.mjs +1 -0
  15. package/board/public/app-arch.js +75 -0
  16. package/board/public/app-asks.js +33 -14
  17. package/board/public/app-cards.js +242 -49
  18. package/board/public/app-core.js +39 -5
  19. package/board/public/app-header.js +24 -6
  20. package/board/public/app-lanes.js +76 -26
  21. package/board/public/app-needs.js +1 -0
  22. package/board/public/app-snapshot.js +45 -11
  23. package/board/public/app-threads.js +13 -11
  24. package/board/public/app.html +4 -1
  25. package/board/public/app.js +131 -20
  26. package/board/public/dialogs.js +114 -116
  27. package/board/public/hub-kit.js +19 -2
  28. package/board/public/hub.css +24 -18
  29. package/board/public/linear-key.js +141 -0
  30. package/board/public/router.js +6 -3
  31. package/board/public/rules-editor.js +1 -1
  32. package/board/public/shell.js +1 -1
  33. package/board/public/sidebar.css +31 -11
  34. package/board/public/sidebar.js +88 -16
  35. package/board/public/style.css +141 -46
  36. package/board/public/toasts.js +6 -4
  37. package/board/public/ui-cards.js +102 -37
  38. package/board/public/ui-header.js +17 -8
  39. package/board/public/ui-hub.js +30 -12
  40. package/board/public/ui-links.js +18 -10
  41. package/board/public/ui-order.js +18 -0
  42. package/board/public/ui-page-header.js +4 -2
  43. package/board/public/ui-questions.js +37 -16
  44. package/board/public/ui-sidebar.js +49 -11
  45. package/board/public/ui-snapshot.js +15 -9
  46. package/board/public/ui-threads.js +25 -16
  47. package/board/public/ui.js +23 -11
  48. package/board/public/view-hub-settings.js +46 -30
  49. package/board/public/view-hub.js +64 -13
  50. package/board/public/view-notes.js +7 -4
  51. package/board/public/view-polling.js +3 -3
  52. package/board/public/view-questions.js +42 -22
  53. package/board/public/view-settings.js +90 -35
  54. package/board/review.mjs +1 -0
  55. package/board/server.mjs +12 -7
  56. package/board/store.mjs +14 -6
  57. package/board/tickets.mjs +3 -3
  58. package/board/worktree.mjs +14 -13
  59. package/docs/hub-session.md +3 -3
  60. package/hooks-plugin/hooks/codex-share-auto.mjs +7 -5
  61. package/hooks-plugin/hooks/watcher-stop.mjs +41 -0
  62. package/hub/awake.mjs +1 -0
  63. package/hub/board-repo.mjs +1 -1
  64. package/hub/boards.mjs +4 -4
  65. package/hub/close-board.mjs +1 -1
  66. package/hub/duration.mjs +1 -0
  67. package/hub/github.mjs +4 -5
  68. package/hub/home.mjs +1 -1
  69. package/hub/hub.mjs +12 -10
  70. package/hub/linear.mjs +2 -2
  71. package/hub/new-board.mjs +2 -2
  72. package/hub/pr-report.mjs +1 -0
  73. package/hub/push.mjs +2 -2
  74. package/hub/readiness.mjs +2 -0
  75. package/hub/repos.mjs +3 -2
  76. package/hub/restart-board.mjs +1 -0
  77. package/hub/routes.mjs +1 -1
  78. package/hub/schedule.mjs +1 -1
  79. package/hub/sessions.mjs +2 -2
  80. package/hub/shell.mjs +2 -1
  81. package/hub/slack.mjs +2 -0
  82. package/hub/stream.mjs +12 -12
  83. package/hub/sw.js +1 -1
  84. package/hub/trust.mjs +2 -0
  85. package/hub/try.mjs +4 -3
  86. package/hub/upgrade.mjs +1 -0
  87. package/hub/usage.mjs +3 -3
  88. package/hub/views.mjs +1 -1
  89. package/package.json +1 -1
  90. package/shared/colors.css +7 -5
  91. package/shared/fake-gh.mjs +54 -0
  92. package/shared/service.mjs +16 -16
  93. package/shared/skills.mjs +2 -0
  94. package/shared/usage-gate.mjs +1 -1
  95. package/starter/public/claude.js +3 -2
  96. package/bin/web-ts.mjs +0 -2
package/hub/try.mjs CHANGED
@@ -1,3 +1,4 @@
1
- var pt=Object.defineProperty;var s=(r,d)=>pt(r,"name",{value:d,configurable:!0});import y from"node:fs";import b from"node:path";import et from"node:os";import ht from"node:http";import{execFile as mt,spawn as yt}from"node:child_process";import{createInterface as it}from"node:readline";import{promisify as wt}from"node:util";import{listBoards as R,readManifest as G,readJSON as rt,saveJSON as gt,GITHUB_STATE as bt}from"./boards.mjs";import{boardRepo as nt,githubFromOrigin as kt}from"./board-repo.mjs";import{buildScriptError as vt}from"../board/build-script.mjs";import{appendThread as St,recordedWorktree as $t}from"../board/store.mjs";class L extends Error{static{s(this,"BuildError")}constructor(d,w=400){super(d),this.status=w}}const Tt=wt(mt),O=s(async(r,...d)=>(await Tt("git",["-C",r,...d],{encoding:"utf8",timeout:3e3})).stdout.trim(),"git"),xt=/errSecInternalComponent|keychain[^\n]*\blocked\b|\blocked\b[^\n]*keychain/i,It=30*6e4,st=s(r=>b.join(r,".kickflip","build"),"scriptAt"),ot=s(r=>y.existsSync(r),"exists"),at=s((r,d)=>b.join(r.dir,"data","try",`${d.split("/").at(-1)}.log`),"logFileOf");function ct(r,d,w){const u=at(r,d);return y.mkdirSync(b.dirname(u),{recursive:!0}),y.writeFileSync(u,w.message+`
2
- `,{mode:384}),y.chmodSync(u,384),u}s(ct,"failureLog");const Ct=s(r=>new Promise(d=>{const w=ht.get({host:"127.0.0.1",port:r,path:"/",timeout:1e3},u=>{u.destroy(),d(!0)});w.on("timeout",()=>w.destroy()),w.on("error",()=>d(!1))}),"probe"),lt=s((r,d)=>{try{process.kill(-r.pid,d)}catch{}},"signalGroup");async function dt(r,d,w){const u=await $t(r.dir,d),[m,E]=d.split("/"),M=r.waves.find($=>$.slug===m)?.wave,j=u?.path||M?.items.find($=>$.id===E)?.worktree;if(typeof j!="string"||!j||j.split(b.sep).includes(".."))return"";let I=u?.repo;const N=(r.github||M?.repo||"").toLowerCase();if(!I&&N){const $=[r.owner?.repo,r.dir,...Object.values(w?.sites||{}).map(k=>k.owner?.repo)];for(const k of new Set($.filter(T=>typeof T=="string"&&b.isAbsolute(T))))try{if(/github\.com[:/]([^/]+\/[^/]+?)(?:\.git)?$/.exec(await O(k,"remote","get-url","origin"))?.[1].toLowerCase()===N){I=await O(k,"rev-parse","--show-toplevel");break}}catch{}}if(!u?.path&&!I||u?.path&&!b.isAbsolute(j))return"";try{const $=[b.join(y.realpathSync(et.homedir()),".claude","kickflip","worktrees")];if(typeof I=="string"&&b.isAbsolute(I))try{$.push(b.join(y.realpathSync(I),".claude","worktrees"))}catch{}const k=b.resolve(I||"/",j),T=y.realpathSync(k);return $.some(_=>k.startsWith(_+b.sep)&&T.startsWith(_+b.sep))&&y.statSync(T).isDirectory()?T:""}catch{return""}}s(dt,"installWorktree");function Ot({now:r=Date.now,answers:d=Ct,log:w=console.log}={}){const u=new Map,m=new Map,E=new Set,M=new Map;let j=!1,I=null;const N=s(()=>new Date(r()).toISOString(),"stamp");function $(t){if(u.has(t.dir))return u.get(t.dir);const i=b.join(t.dir,"data","try.json"),l=rt(i,{kind:null,main:null,cards:{}}),e={kind:l.kind||null,main:l.main||null,cards:{...l.cards}},n={state:e,file:i,written:JSON.stringify(l)};if(u.set(t.dir,n),e.kind==="web")e.cards={},e.main&&e.main.state!=="failed"&&(delete e.main.state,delete e.main.port);else{for(const o of Object.values(e.cards))o.state==="running"&&(delete o.state,delete o.sha);e.main?.state==="running"&&(delete e.main.state,delete e.main.sha)}return k(t),n}s($,"stateOf");function k(t){const i=u.get(t.dir),l=JSON.stringify(i.state);l!==i.written&&(gt(i.file,i.state),i.written=l)}s(k,"persist");const T=s((t,i)=>`${t.dir}\0${i}`,"runKey"),_=s((t,i)=>i==="main"?t.main:t.cards[i],"entryOf");function K(t,i,l){const{state:e}=$(t),n=_(e,i);if(!n)return;const o=Object.hasOwn(l,"state")&&l.state!==n.state;Object.assign(n,l,i!=="main"&&o?{at:N()}:{}),k(t)}s(K,"update");async function Y(t,i,l,e){const{board:n,key:o}=t;if(K(n,o,{state:"failed",port:void 0}),o==="main")return;const c=[];let v=!1;try{for await(const g of it({input:y.createReadStream(t.logFile),crlfDelay:1/0}))v||=xt.test(g),c.push(g),c.length>5&&c.shift()}catch(g){c.push(`Could not read the build log: ${g.message}`)}const S=t.port||($(n).state.kind||(n.waves.some(({wave:g})=>g.items.some(x=>x.install?.url))?"app":"web"))==="web"?"Serve":"Build";await St(n.dir,o,{text:[`${S} failed (exit ${i??1})`,e?.message,l&&`Stopped by ${l}.`,v&&"The login keychain must be unlocked on the Mac.",...c].filter(Boolean).join(`
3
- `)})}s(Y,"failure");function J(t){return t.ending||(lt(t.child,"SIGTERM"),t.ending=new Promise(i=>{const l=s(()=>{clearTimeout(e),clearInterval(n),i()},"finish"),e=setTimeout(()=>{lt(t.child,"SIGKILL"),l()},4e3),n=setInterval(()=>{try{process.kill(-t.child.pid,0)}catch{l()}},20)})),t.ending}s(J,"terminate");async function P(t){t.stopped=!0,clearTimeout(t.deadline),clearInterval(t.poll),await J(t),await t.done,K(t.board,t.key,{state:void 0,port:void 0,...t.ready?{}:{sha:void 0}}),t.ready||M.delete(T(t.board,t.key))}s(P,"stopRun");async function Q(t,i,l,e={}){if(!l)throw new L("This card has no valid worktree.");const n=st(l),o=vt(n);if(o)throw new L(o);if(!y.realpathSync(n).startsWith(y.realpathSync(l)+b.sep))throw new L("Build script must be inside the worktree.");const c=await O(l,"rev-parse","HEAD");if(j)throw new L("The hub is stopping. Try again after it restarts.",503);const{state:v}=$(t),S=_(v,i),g={...i==="main"?{}:S,...e,sha:c,script:!0,state:"running",...i==="main"?{}:{at:N(),head:c}};delete g.port;const x=at(t,i);y.mkdirSync(b.dirname(x),{recursive:!0});const f=y.openSync(x,"w",384);y.fchmodSync(f,384);const p=yt(n,[i,l],{cwd:l,detached:!0,stdio:["ignore","pipe","pipe"],env:{...process.env,KICKFLIP_BOARD:t.route,KICKFLIP_BASE:`/${t.route}/try/${i.split("/").at(-1).toLowerCase()}/`}});i==="main"?v.main=g:v.cards[i]=g,k(t);const C=T(t,i),a={child:p,board:t,key:i,logFile:x,visited:r(),stopped:!1,port:null,ready:!1};m.set(C,a),M.set(C,c);let h,D;const B=s(A=>{D||=new Error(A),J(a)},"fail");p.on("error",A=>{h=A}),p.stdout.on("data",A=>y.writeSync(f,A)),p.stderr.on("data",A=>y.writeSync(f,A));const Z=it({input:p.stdout,crlfDelay:1/0});return Z.on("line",A=>{const H=/^port (\d{1,5})$/.exec(A);if(!H||a.port||a.stopped)return;const F=Number(H[1]);if(F<1||F>65535){B("Build script printed an invalid port.");return}a.port=F,clearTimeout(a.deadline);const z=r()+6e4;let q=!1;const tt=s(async()=>{if(!(q||a.stopped||a.ready||p.exitCode!==null||p.signalCode!==null)){q=!0;try{if(await d(F)){if(a.stopped||D||p.exitCode!==null||p.signalCode!==null)return;a.ready=!0,a.visited=r(),v.kind="web",K(t,i,{state:"ready",port:F}),clearInterval(a.poll)}else r()>=z&&B("Nothing answered on the build script\u2019s port within 60 seconds.")}finally{q=!1}}},"check");a.poll=setInterval(()=>tt().catch(U=>B(U.message)),250),tt().catch(U=>B(U.message))}),a.deadline=setTimeout(()=>B("No port line within 60 minutes."),60*6e4),p.once("exit",()=>{J(a)}),a.done=new Promise(A=>p.once("close",(H,F)=>{clearTimeout(a.deadline),clearInterval(a.poll),Z.close(),y.closeSync(f),(async()=>{await J(a),!a.stopped&&(D||h||H!==0||a.port&&!a.ready?await Y(a,H??1,F,D||h||(a.port&&!a.ready?new Error("Build script exited before its port answered."):null)):a.ready?(i==="main"?(delete v.main.state,delete v.main.port):delete v.cards[i],k(t)):(v.kind="app",K(t,i,{state:"ready"})))})().catch(z=>w(`build /${t.route} ${i}: ${z.message}`)).finally(()=>{m.get(C)===a&&m.delete(C),A()})})),w(`build /${t.route} ${i}: started; log ${x}`),{status:"started"}}s(Q,"launch");async function V(t,{manifest:i=G()}={}){if(j)throw new L("The hub is stopping. Try again after it restarts.",503);if(!t||Array.isArray(t)||typeof t!="object"||Object.keys(t).some(c=>!["route","key"].includes(c))||typeof t.route!="string"||typeof t.key!="string")throw new L("Send only the board route and card key.");const{route:l,key:e}=t;if(!/^[\w.-]+\/[\w.-]+$/.test(e)||e.split("/").some(c=>c==="."||c===".."))throw new L("Card key must be <wave>/<id>.");const n=R(i).find(c=>c.route===l);if(!n)throw new L(`/${l} is not a board.`,404);if(!n.waves.some(({slug:c,wave:v})=>v.items.some(S=>`${c}/${S.id}`===e)))throw new L("There is no item with this card key.",404);const o=T(n,e);if(E.has(o)||m.has(o)&&!m.get(o).ready)throw new L("A build is already running for this card.",409);E.add(o);try{m.has(o)&&await P(m.get(o));const c=await dt(n,e,i);return await Q(n,e,c)}finally{E.delete(o)}}s(V,"start");function X(t,i,{manifest:l=G(),visit:e=!1}={}){const n=R(l).find(c=>c.route===t);if(!n)return null;const o=[...m.values()].find(c=>c.board.dir===n.dir&&c.key.split("/").at(-1).toLowerCase()===i.toLowerCase()&&c.ready&&!c.stopped&&c.child.exitCode===null&&c.child.signalCode===null);return o?(e&&(o.visited=r()),{key:o.key,port:o.port}):null}s(X,"lookup");async function ut({manifest:t=G()}={}){return I||j||(I=(async()=>{const i=R(t),l=rt(bt,{}).prs||{};for(const e of[...m.values()])if(!i.some(n=>n.dir===e.board.dir)||e.key!=="main"&&e.ready&&r()-e.visited>=It){await P(e);const{state:n}=$(e.board);e.key==="main"?n.main=null:delete n.cards[e.key],k(e.board)}for(const e of i){if(j)return;const{state:n}=$(e),o=nt(e.route,t),c=o.branch||"main",v=o.hub?"refs/heads/main":`refs/remotes/origin/${c}`;for(const{slug:S,wave:g}of e.waves)for(const x of g.items){const f=`${S}/${x.id}`;if(!(["done","merged","dropped"].includes(String(x.status).toLowerCase())||["MERGED","CLOSED"].includes(l[`${e.github||g.repo}#${x.pr}`]?.state)))try{const p=await dt(e,f,t);if(!p)continue;const C=await O(p,"rev-parse","HEAD");let a=0;try{a=Number(await O(p,"rev-list","--count",`${v}..HEAD`))}catch{}const h=ot(st(p)),D=n.cards[f]||={};if(Object.assign(D,{head:C,ahead:a,script:h}),k(e),e.auto_build&&h&&a>0&&(D.sha||M.get(T(e,f)))!==C&&D.state!=="running"&&!E.has(T(e,f)))try{await V({route:e.route,key:f},{manifest:t})}catch(B){if([409,503].includes(B.status))continue;throw K(e,f,{sha:C}),await Y({board:e,key:f,logFile:ct(e,f,B)},1),B}}catch(p){w(`try /${e.route} ${f}: ${p.message}`)}}try{if(!o.repo)continue;const S=await O(o.repo,"rev-parse",v);let g=!1;try{await O(o.repo,"cat-file","-e",`${S}:.kickflip/build`),g=!0}catch{}if(!g){const h=m.get(T(e,"main"));h&&await P(h),n.main=null,k(e);continue}const x=n.main;if(x?.state==="running"||x?.sha===S&&(x.state==="failed"||x.state==="ready"&&(n.kind!=="web"||X(e.route,"main",{manifest:t}))))continue;const f=T(e,"main");if(E.has(f))continue;const p=await O(o.repo,"log","-1","--format=%cI",S);let C=e.github;if(!C)try{C=kt(await O(o.repo,"remote","get-url","origin"))}catch{}const a={sha:S,branch:c,slug:C||"",at:p,script:g};E.add(f);try{m.has(f)&&await P(m.get(f));const h=b.join(et.homedir(),".claude","kickflip","try",`${e.route}-main`);y.mkdirSync(b.dirname(h),{recursive:!0}),ot(h)?await O(h,"checkout","--detach","--force",S):await O(o.repo,"worktree","add","--detach",h,S),await Q(e,"main",h,a)}catch(h){if(h.status===503)return;throw n.main={...a,state:"failed"},ct(e,"main",h),k(e),h}finally{E.delete(f)}}catch(S){w(`try /${e.route} main: ${S.message}`)}}})().finally(()=>{I=null})),I}s(ut,"tick");async function ft(){j=!0;const t=Promise.all([...m.values()].map(P));await Promise.all([t,I])}return s(ft,"stop"),{start:V,tick:ut,stop:ft,lookup:X,initialize:s((t=G())=>{for(const i of R(t))$(i)},"initialize")}}s(Ot,"createTryManager");async function Ht({manifest:r=G(),log:d=console.log}={}){for(const w of R(r)){const u=nt(w.route,r);if(u.hub||!u.repo)continue;const m=u.branch||"main";try{await O(u.repo,"fetch","--quiet","--no-tags","origin",`+refs/heads/${m}:refs/remotes/origin/${m}`)}catch(E){d(`fetch /${w.route}: ${E.message}`)}}}s(Ht,"fetchTryBranches");const W=Ot(),Rt=s((...r)=>W.start(...r),"startBuild"),Gt=s((...r)=>W.tick(...r),"tryTick"),Wt=s(()=>W.stop(),"stopTries"),Jt=s((...r)=>W.lookup(...r),"servingTry"),zt=s((...r)=>W.initialize(...r),"initializeTries");export{L as BuildError,Ot as createTryManager,Ht as fetchTryBranches,zt as initializeTries,dt as installWorktree,Jt as servingTry,Rt as startBuild,Wt as stopTries,Gt as tryTick};
1
+ var Bt=Object.defineProperty;var o=(s,u)=>Bt(s,"name",{value:u,configurable:!0});import S from"node:fs";import T from"node:path";import bt from"node:os";import Ft from"node:http";import $t from"node:net";import{execFile as Lt,spawn as Rt}from"node:child_process";import{createInterface as vt}from"node:readline";import{promisify as Gt}from"node:util";import{listBoards as z,readManifest as U,readJSON as V,saveJSON as kt,GITHUB_STATE as Ht,REPO_FETCH as St}from"./boards.mjs";import{boardRepo as st,githubFromOrigin as Tt}from"./board-repo.mjs";import{gitReachFault as Jt,remoteFromInput as Kt,remoteGitEnv as Wt}from"./repos.mjs";import{readiness as qt}from"./readiness.mjs";import{buildScriptError as zt}from"../board/build-script.mjs";import{appendThread as Ut,recordedWorktree as Yt,setItem as Qt}from"../board/store.mjs";class j extends Error{static{o(this,"BuildError")}constructor(u,w=400){super(u),this.status=w}}const ot=Gt(Lt),E=o(async(s,...u)=>(await ot("git",["-C",s,...u],{encoding:"utf8",timeout:3e3})).stdout.trim(),"git"),Vt=/errSecInternalComponent|keychain[^\n]*\blocked\b|\blocked\b[^\n]*keychain/i,H={first:8500,last:8599},at=o(s=>/^\d+$/.test(String(s))&&Number(s)>=H.first&&Number(s)<=H.last,"isTryPort"),Xt=30*6e4,xt=o(s=>T.join(s,".kickflip","build"),"scriptAt"),Ot=o(s=>S.existsSync(s),"exists"),It=o((s,u)=>T.join(s.dir,"data","try",`${u.split("/").at(-1)}.log`),"logFileOf");function Ct(s,u,w){const b=It(s,u);return S.mkdirSync(T.dirname(b),{recursive:!0}),S.writeFileSync(b,w.message+`
2
+ `,{mode:384}),S.chmodSync(b,384),b}o(Ct,"failureLog");const Zt=o(s=>new Promise(u=>{const w=Ft.get({host:"127.0.0.1",port:s,path:"/",timeout:1e3},b=>{b.destroy(),u(!0)});w.on("timeout",()=>w.destroy()),w.on("error",()=>u(!1))}),"probe"),Et=o((s,u)=>{try{process.kill(-s.pid,u)}catch{}},"signalGroup");async function jt(s,u,w){const b=await Yt(s.dir,u),[g,h]=u.split("/"),v=s.waves.find(k=>k.slug===g)?.wave,f=b?.path||v?.items.find(k=>k.id===h)?.worktree;if(typeof f!="string"||!f||f.split(T.sep).includes(".."))return"";let y=b?.repo;const m=(s.github||v?.repo||"").toLowerCase();if(!y&&m){const k=[s.owner?.repo,s.dir,...Object.values(w?.sites||{}).map(P=>P.owner?.repo)];for(const P of new Set(k.filter(x=>typeof x=="string"&&T.isAbsolute(x))))try{if(/github\.com[:/]([^/]+\/[^/]+?)(?:\.git)?$/.exec(await E(P,"remote","get-url","origin"))?.[1].toLowerCase()===m){y=await E(P,"rev-parse","--show-toplevel");break}}catch{}}if(!b?.path&&!y||b?.path&&!T.isAbsolute(f))return"";try{const k=[T.join(S.realpathSync(bt.homedir()),".claude","kickflip","worktrees")];if(typeof y=="string"&&T.isAbsolute(y))try{k.push(T.join(S.realpathSync(y),".claude","worktrees"))}catch{}const P=T.resolve(y||"/",f),x=S.realpathSync(P);return k.some(N=>P.startsWith(N+T.sep)&&x.startsWith(N+T.sep))&&S.statSync(x).isDirectory()?x:""}catch{return""}}o(jt,"installWorktree");function te({now:s=Date.now,answers:u=Zt,log:w=console.log,waiting:b=o(async()=>null,"waiting")}={}){const g=new Map,h=new Map,v=new Set,f=new Set,y=new Map,m=new Map,k=new Map;let P=!1,x=null,N=null,Y=null;const ct=o(()=>new Date(s()).toISOString(),"stamp");function B(t){if(g.has(t.dir))return g.get(t.dir);const e=T.join(t.dir,"data","try.json"),n=V(e,{kind:null,main:null,cards:{}}),r={...at(n.main_port)?{main_port:n.main_port}:{},kind:n.kind||null,main:n.main||null,cards:{...n.cards}},i={state:r,file:e,written:JSON.stringify(n)};g.set(t.dir,i),r.main_port&&m.set(t.dir,r.main_port);const c=Object.entries(r.cards).filter(([,l])=>l.state==="running");for(const[l]of c)f.add(A(t,l));return r.kind==="web"?(r.cards=Object.fromEntries(c),r.main&&r.main.state!=="failed"&&(delete r.main.state,delete r.main.port,delete r.main.https)):r.main?.state==="running"&&(delete r.main.state,delete r.main.sha),F(t),i}o(B,"stateOf");function F(t){const e=g.get(t.dir),n=JSON.stringify(e.state);n!==e.written&&(kt(e.file,e.state),e.written=n)}o(F,"persist");const A=o((t,e)=>`${t.dir}\0${e}`,"runKey"),lt=o((t,e)=>e==="main"?t.main:t.cards[e],"entryOf");function J(t,e,n){const{state:r}=B(t),i=lt(r,e);if(!i)return;const c=Object.hasOwn(n,"state")&&n.state!==i.state;Object.assign(i,n,e!=="main"&&c?{at:ct()}:{}),F(t)}o(J,"update");function Z(t){const e=[...Object.values(t?.sites||{}),...Object.values(t?.closed||{}).map(n=>n.site)];for(const n of e){if(!n?.dir)continue;const r=V(T.join(n.dir,"data","try.json"),{}).main_port;at(r)&&m.set(n.dir,Number(r))}}o(Z,"reserveMains");function Mt(t){const{board:e,key:n}=t,r=A(e,n),i=n==="main"?m.get(e.dir):y.get(r);if(i)return i;const c=new Set([...m.values(),...y.values()]);let l=H.first;for(;c.has(l)&&l<=H.last;)l++;if(l>H.last)throw new Error(`No try ports available (${H.first}\u2013${H.last}). Stop a served card and retry.`);return n==="main"?(m.set(e.dir,l),B(e).state.main_port=l,F(e)):y.set(r,l),l}o(Mt,"publicPort");const tt=o(t=>ot("tailscale",t,{encoding:"utf8",timeout:1e4}),"tailscale");async function dt(t){try{await tt(["serve",`--https=${t}`,"off"])}catch(e){w(`try port ${t}: could not close port: ${(e.stderr||e.message).trim()}`)}}o(dt,"removePort");async function Dt(t){t.https=Mt(t);const e=t.sockets=new Set,n=o(()=>{t.visited=s()},"visit");if(t.relay=$t.createServer(r=>{n();const i=$t.connect(t.port,"127.0.0.1");for(const c of[r,i])e.add(c),c.on("data",n),c.on("error",()=>{r.destroy(),i.destroy()}),c.on("close",()=>{e.delete(c),r.destroy(),i.destroy()});r.pipe(i),i.pipe(r)}),await new Promise((r,i)=>{t.relay.once("error",i),t.relay.listen(0,"127.0.0.1",r)}),!t.stopped){t.exposed=!0;try{await tt(["serve","--bg",`--https=${t.https}`,`http://127.0.0.1:${t.relay.address().port}`])}catch(r){throw new Error(`Couldn't open try port ${t.https}: ${(r.stderr||r.message).trim()}`)}}}o(Dt,"openPort");async function At(t){if(await t.opening?.catch(()=>{}),t.exposed&&await dt(t.https),t.relay){for(const e of t.sockets)e.destroy();await new Promise(e=>t.relay.close(e))}t.keepPort||y.delete(A(t.board,t.key))}o(At,"closePort");function et(t=U()){if(Y)return Y;Z(t);for(const e of z(t))B(e);return Y=(async()=>{try{const{stdout:e}=await tt(["serve","status","--json"]),n=JSON.parse(e);for(const r of Object.keys(n?.TCP||{}))at(r)&&![...h.values()].some(i=>i.https===Number(r))&&await dt(r)}catch(e){w(`try startup cleanup: ${(e.stderr||e.message).trim()}`)}})(),Y}o(et,"initialize");async function ut(t,e,n,r){const{board:i,key:c}=t;if(J(i,c,{state:"failed",port:void 0,https:void 0}),c==="main")return;const l=[];let $=!1;try{for await(const d of vt({input:S.createReadStream(t.logFile),crlfDelay:1/0}))$||=Vt.test(d),l.push(d),l.length>5&&l.shift()}catch(d){l.push(`Could not read the build log: ${d.message}`)}const R=t.port||(B(i).state.kind||(i.waves.some(({wave:d})=>d.items.some(O=>O.install?.url))?"app":"web"))==="web"?"Serve":"Build";await Ut(i.dir,c,{text:[`${R} failed (exit ${e??1})`,r?.message,n&&`Stopped by ${n}.`,$&&"The login keychain must be unlocked on the Mac.",...l].filter(Boolean).join(`
3
+ `)})}o(ut,"failure");function Q(t){return t.ending||(Et(t.child,"SIGTERM"),t.ending=new Promise(e=>{const n=o(()=>{clearTimeout(r),clearInterval(i),e()},"finish"),r=setTimeout(()=>{Et(t.child,"SIGKILL"),n()},4e3),i=setInterval(()=>{try{process.kill(-t.child.pid,0)}catch{n()}},20)})),t.ending}o(Q,"terminate");async function ft(t,e=!1){t.keepPort=e,t.stopped=!0,clearTimeout(t.deadline),clearInterval(t.poll),await Q(t),await t.done}o(ft,"halt");async function K(t,e=!1){await ft(t,e),J(t.board,t.key,{state:void 0,port:void 0,https:void 0,...t.ready?{}:{sha:void 0}}),t.ready||k.delete(A(t.board,t.key))}o(K,"stopRun");async function pt(t,e,n,r={}){if(!n)throw new j("This card has no valid worktree.");const i=xt(n),c=zt(i);if(c)throw new j(c);if(!S.realpathSync(i).startsWith(S.realpathSync(n)+T.sep))throw new j("Build script must be inside the worktree.");const l=await E(n,"rev-parse","HEAD");if(P)throw new j("The hub is stopping. Try again after it restarts.",503);const{state:$}=B(t),R=lt($,e),d={...e==="main"?{}:R,...r,sha:l,script:!0,state:"running",...e==="main"?{}:{at:ct(),head:l}};delete d.port,delete d.https;const O=It(t,e);S.mkdirSync(T.dirname(O),{recursive:!0});const I=S.openSync(O,"w",384);S.fchmodSync(I,384);const p=Rt(i,[e,n],{cwd:n,detached:!0,stdio:["ignore","pipe","pipe"],env:{...process.env,KICKFLIP_BOARD:t.route}});e==="main"?$.main=d:$.cards[e]=d,F(t);const C=A(t,e),a={child:p,board:t,key:e,logFile:O,visited:s(),stopped:!1,port:null,ready:!1};h.set(C,a),k.set(C,l);let _,L;const M=o(D=>{L||=new Error(D),Q(a)},"fail");p.on("error",D=>{_=D}),p.stdout.on("data",D=>S.writeSync(I,D)),p.stderr.on("data",D=>S.writeSync(I,D));const W=vt({input:p.stdout,crlfDelay:1/0});return W.on("line",D=>{const q=/^port (\d{1,5})$/.exec(D);if(!q||a.port||a.stopped)return;const G=Number(q[1]);if(G<1||G>65535){M("Build script printed an invalid port.");return}a.port=G,clearTimeout(a.deadline);const it=s()+6e4;let rt=!1;const gt=o(async()=>{if(!(rt||a.stopped||a.ready||p.exitCode!==null||p.signalCode!==null)){rt=!0;try{if(await u(G)){if(a.stopped||L||p.exitCode!==null||p.signalCode!==null||(a.opening=Dt(a),await a.opening,a.stopped||L||p.exitCode!==null||p.signalCode!==null))return;a.ready=!0,a.visited=s(),$.kind="web",J(t,e,{state:"ready",port:G,https:a.https}),clearInterval(a.poll)}else s()>=it&&M("Nothing answered on the build script\u2019s port within 60 seconds.")}finally{rt=!1}}},"check");a.poll=setInterval(()=>gt().catch(nt=>M(nt.message)),250),gt().catch(nt=>M(nt.message))}),a.deadline=setTimeout(()=>M("No port line within 60 minutes."),60*6e4),p.once("exit",()=>{Q(a)}),a.done=new Promise(D=>p.once("close",(q,G)=>{clearTimeout(a.deadline),clearInterval(a.poll),W.close(),S.closeSync(I),(async()=>{await Q(a),await At(a),a.finished=!0,!a.stopped&&(L||_||q!==0||a.port&&!a.ready?await ut(a,q??1,G,L||_||(a.port&&!a.ready?new Error("Build script exited before its port answered."):null)):a.ready?(e==="main"?(delete $.main.state,delete $.main.port,delete $.main.https):delete $.cards[e],F(t)):($.kind="app",J(t,e,{state:"ready"})))})().catch(it=>w(`build /${t.route} ${e}: ${it.message}`)).finally(()=>{h.get(C)===a&&h.delete(C),D()})})),w(`build /${t.route} ${e}: started; log ${O}`),{status:"started"}}o(pt,"launch");async function ht(t,e,n){const r=A(t,"main");if(v.has(r))throw new j("A build is already running for main.",409);v.add(r);try{h.has(r)&&await K(h.get(r),!0);const i=T.join(bt.homedir(),".claude","kickflip","try",`${t.route}-main`);return S.mkdirSync(T.dirname(i),{recursive:!0}),Ot(i)?await E(i,"checkout","--detach","--force",n.sha):(await E(e,"worktree","prune"),await E(e,"worktree","add","--detach",i,n.sha)),await pt(t,"main",i,n)}catch(i){throw i.status!==503&&(B(t).state.main={...n,state:"failed"},Ct(t,"main",i),F(t)),i}finally{v.delete(r)}}o(ht,"launchMain");async function mt(t,{manifest:e=U()}={}){if(await et(e),Z(e),P)throw new j("The hub is stopping. Try again after it restarts.",503);if(!t||Array.isArray(t)||typeof t!="object"||Object.keys(t).some(d=>!["route","key"].includes(d))||typeof t.route!="string"||typeof t.key!="string")throw new j("Send only the board route and card key.");const{route:n,key:r}=t;if(r!=="main"&&(!/^[\w.-]+\/[\w.-]+$/.test(r)||r.split("/").some(d=>d==="."||d==="..")))throw new j("Card key must be <wave>/<id>.");const i=z(e).find(d=>d.route===n);if(!i)throw new j(`/${n} is not a board.`,404);if(r!=="main"&&!i.waves.some(({slug:d,wave:O})=>O.items.some(I=>`${d}/${I.id}`===r)))throw new j("There is no item with this card key.",404);const c=await b();if(c)throw new j(c,409);const l=A(i,r),$=h.get(l);if(v.has(l)||$&&!$.ready&&!$.finished)throw new j(`A build is already running for ${r==="main"?"main":"this card"}.`,409);if(r==="main"){const d=B(i).state.main;if(d?.state==="running")throw new j("A build is already running for main.",409);if(d?.state!=="failed"||!d.sha)throw new j("Main has no failed build to retry.",409);const{sha:O,branch:I,slug:p,at:C,script:a}=d;return ht(i,st(n,e).repo,{sha:O,branch:I,slug:p,at:C,script:a})}v.add(l);const R=f.delete(l);try{h.has(l)&&await K(h.get(l),!0);const d=await jt(i,r,e);return await pt(i,r,d)}catch(d){throw y.delete(l),R&&d.status!==503&&J(i,r,{state:void 0,sha:void 0}),d}finally{v.delete(l)}}o(mt,"start");function wt(t,e,{manifest:n=U()}={}){const r=z(n).find(c=>c.route===t);if(!r)return null;const i=[...h.values()].find(c=>c.board.dir===r.dir&&c.key.split("/").at(-1).toLowerCase()===e.toLowerCase()&&c.ready&&!c.stopped&&c.child.exitCode===null&&c.child.signalCode===null);return i?{key:i.key,port:i.port}:null}o(wt,"lookup");async function yt({manifest:t=U(),routes:e}={}){if(await et(t),Z(t),P)return x;if(x&&e){if(!N){const n={routes:new Set,manifest:t};n.done=x.catch(()=>{}).then(()=>(N=null,yt({manifest:n.manifest,routes:[...n.routes]}))),N=n}N.manifest=t;for(const n of e)N.routes.add(n);return N.done}return x||(x=(async()=>{const n=z(t),r=V(Ht,{}).prs||{};for(const i of[...h.values()])if(!n.some(c=>c.dir===i.board.dir)||i.key!=="main"&&i.ready&&s()-i.visited>=Xt){await K(i);const{state:c}=B(i.board);i.key==="main"?c.main=null:delete c.cards[i.key],F(i.board)}for(const i of n){if(P)return;if(e&&!e.includes(i.route))continue;const{state:c}=B(i),l=st(i.route,t),$=l.branch||"main",R=l.local?"refs/heads/main":`refs/remotes/origin/${$}`;for(const{slug:d,wave:O}of i.waves)for(const I of O.items){const p=`${d}/${I.id}`;if(!(["done","merged","dropped"].includes(String(I.status).toLowerCase())||["MERGED","CLOSED"].includes(r[`${i.github||O.repo}#${I.pr}`]?.state)))try{const C=A(i,p),a=await jt(i,p,t);if(a){const M=await E(a,"rev-parse","HEAD");let W=0;try{W=Number(await E(a,"rev-list","--count",`${R}..HEAD`))}catch{}Object.assign(c.cards[p]||={},{head:M,ahead:W,script:Ot(xt(a))}),F(i)}else if(!f.has(C))continue;const _=c.cards[p];if((f.has(C)||i.auto_build&&_.script&&_.ahead>0&&(_.sha||k.get(C))!==_.head&&_.state!=="running")&&!v.has(C))try{await mt({route:i.route,key:p},{manifest:t})}catch(M){if([409,503].includes(M.status))continue;throw J(i,p,{sha:_.head}),await ut({board:i,key:p,logFile:Ct(i,p,M)},1),M}}catch(C){w(`try /${i.route} ${p}: ${C.message}`)}}try{if(!l.repo||await b())continue;const d=await E(l.repo,"rev-parse",R);let O=!1;try{await E(l.repo,"cat-file","-e",`${d}:.kickflip/build`),O=!0}catch{}if(!O){const L=h.get(A(i,"main"));L&&await K(L),c.main=null,F(i);continue}const I=c.main;if(I?.state==="running"||I?.sha===d&&(I.state==="failed"||I.state==="ready"&&(c.kind!=="web"||wt(i.route,"main",{manifest:t}))))continue;const p=A(i,"main");if(v.has(p))continue;const C=await E(l.repo,"log","-1","--format=%cI",d);let a=i.github;if(!a)try{a=Tt(await E(l.repo,"remote","get-url","origin"))}catch{}const _={sha:d,branch:$,slug:a||"",at:C,script:O};await ht(i,l.repo,_)}catch(d){w(`try /${i.route} main: ${d.message}`)}}})().finally(()=>{x=null}),x)}o(yt,"tick");async function Nt(){P=!0;const t=Promise.all([...h.values()].map(e=>e.ready?K(e):ft(e)));await Promise.all([t,x])}return o(Nt,"stop"),{start:mt,tick:yt,stop:Nt,lookup:wt,initialize:et}}o(te,"createTryManager");const Pt=1e4,_t=o(async(s,...u)=>(await ot("git",["-C",s,...u],{encoding:"utf8",timeout:Pt,env:Wt()})).stdout,"originGit");async function ye({manifest:s=U(),log:u=console.log,routes:w}={}){const b=w?V(St,{}):{};for(const g of z(s)){if(w&&!w.includes(g.route))continue;const h=st(g.route,s);if(h.local||!h.repo)continue;const v=h.branch||"main",f=new Date().toISOString(),y=Date.now();try{await _t(h.repo,"fetch","--quiet","--no-tags","origin",`+refs/heads/${v}:refs/remotes/origin/${v}`),b[g.route]={at:f,ok:!0}}catch(m){u(`fetch /${g.route} after ${Date.now()-y} ms: ${m.message}`);let k=g.github;if(!k&&!g.remote)try{k=Tt(await E(h.repo,"remote","get-url","origin"))}catch{}b[g.route]={at:f,ok:!1,...Jt(m.killed?`git fetch timed out after ${Pt/1e3} s`:m.stderr,{github:k||"",remote:g.remote,repo:h.repo})};continue}if(g.remote)try{await ee(g,h.repo,v,u)}catch(m){u(`pushed branches /${g.route}: ${m.message}`)}}kt(St,b)}o(ye,"fetchTryBranches");async function ee(s,u,w,b){const g=s.waves.flatMap(({slug:f,wave:y})=>y.items.filter(m=>m.pushed?.branch&&!m.pr&&!["done","merged","dropped"].includes(String(m.status).toLowerCase())).map(m=>({key:`${f}/${m.id}`,branch:m.pushed.branch})));if(!g.length)return;const h=new Map((await _t(u,"ls-remote","--heads","origin")).split(`
4
+ `).map(f=>/^([\da-f]+)\trefs\/heads\/(.+)$/.exec(f)).filter(Boolean).map(f=>[f[2],f[1]])),v=Kt(s.remote)?.host||"the remote";for(const f of g){let y=h.get(f.branch);if(!y)try{y=await E(u,"rev-parse","--verify","--quiet",`refs/heads/${f.branch}^{commit}`)}catch{}let m=!1;if(y)try{await E(u,"merge-base","--is-ancestor",y,`refs/remotes/origin/${w}`),m=!0}catch{}if(!(!m&&h.has(f.branch)))try{await Qt(s.dir,f.key,{status:m?"merged":"done",force:!0,text:m?`${f.branch} is merged into ${w} on ${v}, so this card is Done.`:`${f.branch} is gone from ${v}, so this card is Done.`})}catch(k){b(`pushed ${f.key}: ${k.message}`)}}}o(ee,"settlePushed");const X=te({waiting:o(()=>qt.waiting("tailscale"),"waiting")}),ge=o((...s)=>X.start(...s),"startBuild"),be=o((...s)=>X.tick(...s),"tryTick"),$e=o(()=>X.stop(),"stopTries"),ve=o((...s)=>X.initialize(...s),"initializeTries");export{j as BuildError,H as TRY_PORTS,te as createTryManager,ye as fetchTryBranches,ve as initializeTries,jt as installWorktree,at as isTryPort,ge as startBuild,$e as stopTries,be as tryTick};
@@ -0,0 +1 @@
1
+ var E=Object.defineProperty;var t=(r,s)=>E(r,"name",{value:s,configurable:!0});import h from"node:fs";import d from"node:path";const _="https://registry.npmjs.org/@lovek4d%2Fkickflip/latest",C=60*6e4,v=t(r=>String(r).split("-")[0].split(".").map(Number),"parts");function F(r,s){const[l,u]=[v(r),v(s)];for(let n=0;n<3;n++)if((l[n]||0)!==(u[n]||0))return(l[n]||0)>(u[n]||0);return!1}t(F,"newer");function L({root:r,fetch:s=globalThis.fetch,builders:l,run:u,log:n=t(()=>{},"log"),changed:c=t(()=>{},"changed")}){const k=h.existsSync(d.join(r,".git"));let i=null,f=!1,a=!1,p=0,o=null;const y=t(()=>JSON.parse(h.readFileSync(d.join(r,"package.json"),"utf8")).version,"installed");async function S(){if(!k){try{const e=await s(_,{signal:AbortSignal.timeout(15e3)});if(!e.ok)throw new Error(`npm answered ${e.status}`);i=(await e.json()).version}catch(e){n(`kickflip update check failed: ${e.message}`)}c()}}t(S,"check");function g(){return k||!i||!F(i,y())?null:a?{state:"updating",version:i}:f?{state:"waiting",version:i,builders:p}:o?{state:"failed",version:i,error:o}:{state:"available",version:i}}t(g,"state");let m=Promise.resolve();const w=t(()=>m=m.catch(()=>{}).then(b),"tick");async function b(){if(!(!f||a)){if(p=(await l()).length,p){c();return}f=!1,a=!0,o=null,c(),u().then(({ok:e,error:x})=>{e||(o=x)},e=>{o=e.message}).finally(()=>{a=!1,c()})}}t(b,"tickOnce");async function j(){if(!g())throw Object.assign(new Error("kickflip is up to date."),{status:409});return a||(f=!0),await w(),g()}return t(j,"request"),{check:S,state:g,request:j,tick:w}}t(L,"upgrader");export{C as CHECK_MS,_ as LATEST_URL,F as newer,L as upgrader};
package/hub/usage.mjs CHANGED
@@ -1,3 +1,3 @@
1
- var N=Object.defineProperty;var u=(e,t)=>N(e,"name",{value:t,configurable:!0});import f from"node:fs";import _ from"node:os";import m from"node:path";const h=6,b=180,D={five_hour:5*36e5,seven_day:7*864e5},S=u(e=>{try{return JSON.parse(f.readFileSync(e,"utf8"))}catch{return null}},"readJson"),y=u(e=>e==null||e===""||!Number.isFinite(Number(e))?null:Number(e),"num"),g=u(e=>{if(e==null||e==="")return null;const s=typeof e=="number"||/^\d+(\.\d+)?$/.test(String(e))?Number(e)>1e12?Number(e):Number(e)*1e3:Date.parse(String(e));return Number.isFinite(new Date(s).getTime())?new Date(s).toISOString():null},"isoOf"),w=u((e,t)=>({used_percentage:y(e),resets_at:g(t)}),"win");function O(e){let t;try{t=f.openSync(e,"r");const s=f.fstatSync(t).size;for(const n of[256*1024,2*1024*1024]){const r=Math.max(0,s-n),a=Buffer.alloc(s-r);f.readSync(t,a,0,a.length,r);const i=a.toString("utf8").split(`
2
- `);r>0&&i.shift();for(let c=i.length-1;c>=0;c--)if(i[c].includes("rate_limits"))try{const d=JSON.parse(i[c]),o=d?.payload?.rate_limits??d?.rate_limits;if(o?.primary)return{rl:o}}catch{}if(r===0)break}}catch{}finally{if(t!==void 0)try{f.closeSync(t)}catch{}}return null}u(O,"lastRateLimits");function k(e){const t=[],s=u(n=>{try{return f.readdirSync(n).sort().reverse()}catch{return[]}},"kids");e:for(const n of s(e))for(const r of s(m.join(e,n)))for(const a of s(m.join(e,n,r))){const i=m.join(e,n,r,a);for(const c of s(i))if(/^rollout-.*\.jsonl$/.test(c))try{t.push({file:m.join(i,c),mtime:f.statSync(m.join(i,c)).mtimeMs})}catch{}if(t.length>=h*2)break e}return t.sort((n,r)=>r.mtime-n.mtime).slice(0,h)}u(k,"newestRollouts");function x(e){const t=[];for(const{file:o,mtime:l}of k(e)){const p=O(o);p&&t.push({...p,mtime:l,resets:y(p.rl.primary.resets_at)})}if(!t.length)return null;const s=Math.max(...t.map(o=>o.resets??-1/0)),n=t.filter(o=>o.resets!=null&&Math.abs(o.resets-s)<=b),r=u(o=>n.reduce((l,p)=>(y(p.rl[o]?.used_percent)??-1)>(y(l?.rl[o]?.used_percent)??-1)?p:l,null),"pick");if(!n.length)return null;const a=r("primary"),i=r("secondary"),c=n.reduce((o,l)=>l.mtime>o.mtime?l:o),d=u((o,l)=>o?w(o.rl[l].used_percent,o.rl[l].resets_at):null,"w");return{five_hour:d(a,"primary"),seven_day:d(i,"secondary"),at:new Date(c.mtime).toISOString()}}u(x,"scanCodex");function j(e,t){if(!e||typeof e!="object"||Array.isArray(e))return null;const s=g(e.at),n=u(r=>{const a=e[r];if(!a)return null;const i=w(a.used_percentage,a.resets_at);if(i.used_percentage==null)return null;const c=!s||t-Date.parse(s)>D[r]?"stale":i.resets_at&&t>=Date.parse(i.resets_at)?"reset":"live";return{...i,used_percentage:c==="reset"?0:i.used_percentage,state:c}},"window");return{at:s,five_hour:n("five_hour"),seven_day:n("seven_day")}}u(j,"readingOf");function W({home:e=_.homedir(),now:t=Date.now,cacheMs:s=6e4}={}){const n=m.join(e,".codex","sessions");let r=null,a=-1/0;return({codexShare:i="off"}={})=>{const c=t(),d=j(S(m.join(e,".claude","kickflip","usage.json"))?.claude,c);return i==="off"?{claude:d,codex:null}:(c-a>=s&&(r=x(n),a=c),{claude:d,codex:j(r||{at:null},c)})}}u(W,"usageReader");async function $({gate:e,home:t=_.homedir(),resume:s}){const n=m.join(t,".claude","kickflip","usage-gate-state.json"),r=S(n);r?.state==="paused"&&e.state==="normal"&&await s(r.window),f.mkdirSync(m.dirname(n),{recursive:!0});const a=`${n}.${process.pid}.tmp`;f.writeFileSync(a,JSON.stringify(e)+`
3
- `),f.renameSync(a,n)}u($,"checkUsagePause");export{$ as checkUsagePause,W as usageReader};
1
+ var N=Object.defineProperty;var a=(e,t)=>N(e,"name",{value:t,configurable:!0});import f from"node:fs";import _ from"node:os";import m from"node:path";const S=6,b=180,x={five_hour:5*36e5,seven_day:7*864e5},h=a(e=>{try{return JSON.parse(f.readFileSync(e,"utf8"))}catch{return null}},"readJson"),y=a(e=>e==null||e===""||!Number.isFinite(Number(e))?null:Number(e),"num"),g=a(e=>{if(e==null||e==="")return null;const n=typeof e=="number"||/^\d+(\.\d+)?$/.test(String(e))?Number(e)>1e12?Number(e):Number(e)*1e3:Date.parse(String(e));return Number.isFinite(new Date(n).getTime())?new Date(n).toISOString():null},"isoOf"),w=a((e,t)=>({used_percentage:y(e),resets_at:g(t)}),"win");function k(e){let t;try{t=f.openSync(e,"r");const n=f.fstatSync(t).size;for(const r of[256*1024,2*1024*1024]){const s=Math.max(0,n-r),u=Buffer.alloc(n-s);f.readSync(t,u,0,u.length,s);const i=u.toString("utf8").split(`
2
+ `);s>0&&i.shift();for(let c=i.length-1;c>=0;c--)if(i[c].includes("rate_limits"))try{const d=JSON.parse(i[c]),o=d?.payload?.rate_limits??d?.rate_limits;if(o?.primary)return{rl:o}}catch{}if(s===0)break}}catch{}finally{if(t!==void 0)try{f.closeSync(t)}catch{}}return null}a(k,"lastRateLimits");function D(e){const t=[],n=a(r=>{try{return f.readdirSync(r).sort().reverse()}catch{return[]}},"kids");e:for(const r of n(e))for(const s of n(m.join(e,r)))for(const u of n(m.join(e,r,s))){const i=m.join(e,r,s,u);for(const c of n(i))if(/^rollout-.*\.jsonl$/.test(c))try{t.push({file:m.join(i,c),mtime:f.statSync(m.join(i,c)).mtimeMs})}catch{}if(t.length>=S*2)break e}return t.sort((r,s)=>s.mtime-r.mtime).slice(0,S)}a(D,"newestRollouts");function O(e){const t=[];for(const{file:o,mtime:l}of D(e)){const p=k(o);p&&t.push({...p,mtime:l,resets:y(p.rl.primary.resets_at)})}if(!t.length)return null;const n=Math.max(...t.map(o=>o.resets??-1/0)),r=t.filter(o=>o.resets!=null&&Math.abs(o.resets-n)<=b),s=a(o=>r.reduce((l,p)=>(y(p.rl[o]?.used_percent)??-1)>(y(l?.rl[o]?.used_percent)??-1)?p:l,null),"pick");if(!r.length)return null;const u=s("primary"),i=s("secondary"),c=r.reduce((o,l)=>l.mtime>o.mtime?l:o),d=a((o,l)=>o?w(o.rl[l].used_percent,o.rl[l].resets_at):null,"w");return{five_hour:d(u,"primary"),seven_day:d(i,"secondary"),at:new Date(c.mtime).toISOString()}}a(O,"scanCodex");function j(e,t){if(!e||typeof e!="object"||Array.isArray(e))return null;const n=g(e.at),r=a(s=>{const u=e[s];if(!u)return null;const i=w(u.used_percentage,u.resets_at);if(i.used_percentage==null)return null;const c=!n||t-Date.parse(n)>x[s]?"stale":i.resets_at&&t>=Date.parse(i.resets_at)?"reset":"live";return{...i,used_percentage:c==="reset"?0:i.used_percentage,state:c}},"window");return{at:n,five_hour:r("five_hour"),seven_day:r("seven_day")}}a(j,"readingOf");function J({home:e=_.homedir(),now:t=Date.now,cacheMs:n=6e4}={}){const r=m.join(e,".codex","sessions");let s=null,u=-1/0;return({codexShare:i="off"}={})=>{const c=t(),d=j(h(m.join(e,".claude","kickflip","usage.json"))?.claude,c);return i==="off"?{claude:d,codex:null}:(c-u>=n&&(s=O(r),u=c),{claude:d,codex:j(s||{at:null},c)})}}a(J,"usageReader");function R(e,{home:t=_.homedir()}={}){if(typeof e!="string"||!/^[\w-]+$/.test(e))return null;const n=h(m.join(t,".claude","kickflip","context",`${e}.json`));return y(n?.percent)==null||!n.at?null:{percent:y(n.percent),at:n.at}}a(R,"contextReading");async function W({gate:e,home:t=_.homedir(),resume:n}){const r=m.join(t,".claude","kickflip","usage-gate-state.json"),s=h(r);s?.state==="paused"&&e.state==="normal"&&await n(s.window),f.mkdirSync(m.dirname(r),{recursive:!0});const u=`${r}.${process.pid}.tmp`;f.writeFileSync(u,JSON.stringify(e)+`
3
+ `),f.renameSync(u,r)}a(W,"checkUsagePause");export{W as checkUsagePause,R as contextReading,J as usageReader};
package/hub/views.mjs CHANGED
@@ -1 +1 @@
1
- var P=Object.defineProperty;var l=(e,t)=>P(e,"name",{value:t,configurable:!0});import{clock as m,windowAt as q,holdLine as x,checksIn as z,checksADay as I,holdsOvernight as C,daySegments as M,splitPoint as H,snoozeEnd as U}from"./schedule.mjs";import{siteIcon as R}from"./icons.mjs";import{codexStatus as L}from"../shared/codex.mjs";const y=l(e=>e.charAt(0).toUpperCase()+e.slice(1),"capital"),g=l((e,t)=>`${e} ${t}${e===1?"":"s"}`,"plural"),b=l(e=>e>=60&&e%60===0?`${e/60} h`:`${e} min`,"every"),p=l(e=>{const[t,o]=e.split(":").map(Number),s=new Date;return s.setHours(t,o,0,0),m(s,s)},"timeOfDay"),V=l(e=>{const[t,o]=e.split(":").map(Number);return t*60+o},"minuteOf"),$=l(e=>`${(e/1440*100).toFixed(3)}%`,"pct"),v=l((e,t)=>`checks every ${e.window.every_minutes} min until ${m(e.window.endsAt,t)}${e.hold?`; ${x(e.hold,t)}`:""}`,"nowLine"),N=l(e=>[`${g(e.windows.length,"window")}, ${I(e)} checks a day`,C(e)&&"held overnight",e.wakes.length?`wakes ${e.wakes.map(p).join(", ")}`:"no wakes"].filter(Boolean).join(" \xB7 "),"pollingSummary"),F=l((e,t)=>e.count?`${e.count} push${e.count===1?"":"es"} queued; next send ${m(e.nextFlushAt,t)}.`:"No pushes queued.","queuedLine");function T({running:e=!1,nextAt:t,last:o=null},s){if(e)return"Checking GitHub now";const n=`Next check ${m(t,s)}`;if(!o)return n;const d=o.failed.length?"could not reach GitHub for every PR":o.watched?`${g(o.watched,"PR")}, ${o.changes.length?`${o.changes.length} changed`:"no change"}`:"no PRs watched";return`${n} \xB7 last ${m(new Date(o.at),s)}, ${d}`}l(T,"pollLine");function O(e,t,o){return{title:y(v(e,o)),lines:[T(t,o),F(e,o)],count:e.count}}l(O,"pollingNow");function B(e,t){const o={port:"Down \xB7 not answering",files:"Down \xB7 folder missing"}[e.serves]||"Down \xB7 restarting";if(e.state!=="up")return{tone:e.state==="down"?"down":"wait",word:e.state==="down"?o:y(e.state),lines:[]};if(!t)return{tone:"ok",word:"Up",lines:[]};const s=t.filter(r=>!r.ok),n=s.filter(r=>r.state==="broken"),d=t.find(r=>r.link==="events"),a=d?.listen==="asleep"?"asleep":d?.ok?"listening":"not listening",w=n.length?`${g(n.length,"link")} broken \xB7 ${a}`:s.length?`Up \xB7 ${a} \xB7 ${s.length} settling`:`Up \xB7 ${a} \xB7 every link passes`;return{tone:n.length?"down":s.length?"wait":"ok",word:w,lines:s.map(r=>({link:r.link,detail:`${r.state==="broken"?"":"settling: "}${r.detail}`}))}}l(B,"siteHealth");const A=[{value:"always",label:"Always on",description:"Codex builds and reviews; past the pause line, Opus builders take over until it resets, and while Codex is out of usage that work pauses until a usage-reset check succeeds."},{value:"auto",label:"Auto",description:"Codex builds and reviews; while Codex is past the pause line or out of usage, Opus builders take over until it resets."},{value:"off",label:"Off",description:"No Codex."}],G=l((e,t)=>({usable:t.usable,tag:t.usable?null:y(t.reason),share:e,choices:A.map(({value:o,label:s})=>({value:o,label:s,on:o===e})),description:t.usable?A.find(o=>o.value===e).description:t.words,fix:t.fix,note:t.usable?"Applies to every board. A session's own /codex-share overrides it for that session.":null}),"codexView");function ee({now:e,st:t,poll:o,sidebar:s,rows:n,links:d=new Map,code:a={},host:w="",rules:r=[],remembered:f=new Map,codexShare:i="off",codex:c=L({installed:!1})}){const h=new Set(s.sites.filter(u=>u.board).map(u=>u.route)),k=a.running&&a.running===a.onDisk?` \xB7 runs the code in its files (${a.onDisk})`:"",_=l(u=>({route:u.route,title:u.title,icon:R(u.route,u),href:`/${encodeURIComponent(u.route)}/`,...B(u,d.get(u.route)),remembered:f.get(u.route)||[]}),"site"),D=[["Boards",n.filter(u=>h.has(u.route))],["Pages",n.filter(u=>!h.has(u.route))]].filter(([,u])=>u.length).map(([u,E])=>({label:u,sites:E.map(_)}));return{hub:{tone:s.hub.tone,word:s.hub.word,detail:`${s.hub.detail}${k}`},groups:D,usage:s.usage,pause:s.pause,codex:G(i,c),empty:n.length===0,polling:{...O(t,o,e),summary:N(t.settings)},notifications:{on:t.settings.notifications.on,text:s.notifications.text,device:v(t,e)},hold:t.hold&&t.hold.why!=="off"?{text:`Notifications ${x(t.hold,e)}`,end:t.hold.why==="snoozed"}:null,rules:r.length?g(r.length,"rule"):"None yet",sites:g(n.length,"site"),host:w,total:s.total}}l(ee,"hubView");const j=[["3h","3h"],["6h","6h"],["1d","1d"],["3d","3d"],["monday","Monday"]],te="That snooze ends in the past or more than 30 days on. Pick another time.",K=l((e,t)=>e.on&&e.snoozed_until&&new Date(e.snoozed_until)>t?new Date(e.snoozed_until):null,"snoozedUntil");function ne({now:e,st:t,rules:o=[],route:s={atMac:!1,desktops:0},linear:n={set:!1}}){const d=t.settings,a=d.notifications,[w,r]=[d.windows[0],d.windows.at(-1)],f=K(a,e),i=t.count?` The ${t.count} queued push${t.count===1?" is":"es are"} dropped now.`:"",c=j.map(([h,k])=>({value:h,label:k,until:`until ${m(U(e,h,d),e)}`}));return{snoozed:f?{title:`Snoozed until ${m(f,e)}`}:null,device:v(t,e),on:{on:a.on,sub:a.on?"On. Turn off to drop every push, urgent ones too.":"Off. Every push is dropped, urgent ones too. Needs you and the badge still update.",confirm:`Turn off notifications? Every push is dropped until you turn them back on, urgent ones too.${i} Needs you and the badge still update.`},presence:a.on?s.atMac?s.desktops?{title:"At the host",sub:"Pushes go to the host only. Prompts, faults and urgent pushes still reach the phone."}:{title:"At the host",sub:"No notifications on the host, so pushes still go to the phone."}:{title:"Away from the host",sub:"Pushes go to every device. At the host means unlocked and used in the last 3 minutes."}:null,snooze:a.on&&!f?{choices:c}:null,weekends:{on:a.weekends_off,sub:`Pushes wait from Friday ${p(r.from)} until Monday ${p(w.from)}. Urgent ones still go.`},polling:N(d),rules:o,linear:n.set?{set:!0,title:`${n.workspace} workspace`,sub:"Key \u2022\u2022\u2022\u2022\u2022\u2022\u2022\u2022",summary:`Key set \xB7 ${n.workspace} workspace`}:{set:!1,title:"No key",sub:"Paste a personal API key from Linear\u2019s Settings \u2192 Security & access.",summary:"No key"}}}l(ne,"settingsView");const W=[5,10,15,30,60,120],Z=l(e=>Math.round(100-Math.log(e/5)/Math.log(24)*70),"shade"),S=l((e,t)=>e.windows.length===1?"All day":`${p(e.windows[t].from)} \u2013 ${p(e.windows[(t+1)%e.windows.length].from)}`,"windowRange");function oe({now:e,st:t,poll:o,select:s=null}){const n=t.settings,d=n.windows.findIndex(i=>i.from===s),a=d>=0?d:n.windows.findIndex(i=>i.from===q(e,n).from),w=n.windows.length===1,[r,f]=[n.windows[0],n.windows.at(-1)];return{select:n.windows[a].from,now:O(t,o,e),day:{segments:M(n).map(i=>{const c=n.windows[i.window];return{w:c.from,left:$(i.start),width:$(i.end-i.start),shade:`${Z(c.every_minutes)}%`,held:i.held,label:i.end-i.start>=110?b(c.every_minutes).replace(" min","m").replace(" h","h"):"",aria:`${S(n,i.window)}, every ${b(c.every_minutes)}${i.held?", pushes held":""}`}}),pins:n.wakes.map(i=>({left:$(V(i)),title:`Wake ${p(i)}`})),now:$(e.getHours()*60+e.getMinutes()),held:C(n),wakes:n.wakes.length>0},windows:n.windows.map((i,c)=>({from:i.from,range:S(n,c),ends:w?"\u2014":p(n.windows[(c+1)%n.windows.length].from),every:[...new Set([...W,i.every_minutes])].sort((h,k)=>h-k).map(h=>({minutes:h,label:b(h),on:h===i.every_minutes})),checks:`${g(z(n,c),"check")} in this window`,split:!!H(n,c),remove:!w})),hold:{on:n.hold_overnight,sub:w?"With one window there is no overnight to hold.":`Pushes queued after ${p(f.from)} wait until ${p(r.from)}. GitHub checks still run.`},wakes:n.wakes.map(i=>({value:i,label:p(i)}))}}l(oe,"pollingView");function se(e){return e?e.ok?{sent:e.sent,count:e.count,ok:!0,message:`Sent ${e.count} queued push${e.count===1?"":"es"}.`}:{sent:e.sent,count:e.count,ok:!1,message:"Could not send the queue. It stays queued for the next check."}:{sent:0,count:0,ok:!0,message:"Nothing was queued."}}l(se,"flushView");export{te as SNOOZE_REFUSED,y as capital,b as every,se as flushView,ee as hubView,v as nowLine,g as plural,T as pollLine,N as pollingSummary,oe as pollingView,ne as settingsView,B as siteHealth};
1
+ var F=Object.defineProperty;var u=(e,t)=>F(e,"name",{value:t,configurable:!0});import{clock as w,windowAt as K,holdLine as D,checksIn as T,checksADay as B,holdsOvernight as R,daySegments as G,splitPoint as j,sendWords as Y}from"./schedule.mjs";import{CHOICES as Z,FOREVER as x,durationEnd as X,endOf as J}from"./duration.mjs";import{siteIcon as Q}from"./icons.mjs";import{codexStatus as ee}from"../shared/codex.mjs";const O=u(e=>e.charAt(0).toUpperCase()+e.slice(1),"capital"),g=u((e,t)=>`${e} ${t}${e===1?"":"s"}`,"plural"),E=u(e=>e>=60&&e%60===0?`${e/60} h`:`${e} min`,"every"),f=u(e=>{const[t,s]=e.split(":").map(Number),o=new Date;return o.setHours(t,s,0,0),w(o,o)},"timeOfDay"),te=u(e=>{const[t,s]=e.split(":").map(Number);return t*60+s},"minuteOf"),y=u(e=>`${(e/1440*100).toFixed(3)}%`,"pct"),A=u((e,t)=>`checks every ${e.window.every_minutes} min until ${w(e.window.endsAt,t)}${e.hold?`; ${D(e.hold,t)}`:""}`,"nowLine"),_=u(e=>[`${g(e.windows.length,"window")}, ${B(e)} checks a day`,R(e)&&"held overnight",e.wakes.length?`wakes ${e.wakes.map(f).join(", ")}`:"no wakes"].filter(Boolean).join(" \xB7 "),"pollingSummary"),ne=u((e,t)=>e.count?`${e.count} push${e.count===1?"":"es"} queued; next send ${Y(e.nextFlushAt,t)}.`:"No pushes queued.","queuedLine");function oe({running:e=!1,nextAt:t,last:s=null},o){if(e)return"Checking GitHub now";const n=`Next check ${w(t,o)}`;if(!s)return n;const r=s.failed.length?"could not reach GitHub for every PR":s.watched?`${g(s.watched,"PR")}, ${s.changes.length?`${s.changes.length} changed`:"no change"}`:"no PRs watched";return`${n} \xB7 last ${w(new Date(s.at),o)}, ${r}`}u(oe,"pollLine");function N(e,t,s){return{title:O(A(e,s)),lines:[oe(t,s),ne(e,s)],count:e.count}}u(N,"pollingNow");function se(e,t){const s={port:"Down \xB7 not answering",files:"Down \xB7 folder missing"}[e.serves]||"Down \xB7 restarting";if(e.state!=="up")return{tone:e.state==="down"?"down":"wait",word:e.state==="down"?s:O(e.state),lines:[]};if(!t)return{tone:"ok",word:"Up",lines:[]};const o=t.filter(a=>!a.ok),n=o.filter(a=>a.state==="broken"),r=t.find(a=>a.link==="events"),d=r?.listen==="asleep"||r?.listen==="waking"?r.listen:r?.ok?"listening":"not listening",c=n.length?`Degraded \xB7 ${d}`:o.length?`Up \xB7 ${d} \xB7 ${o.length} settling`:`Up \xB7 ${d} \xB7 every link passes`;return{tone:o.length?"wait":"ok",word:c,lines:o.map(a=>({link:a.link,detail:`${a.state==="broken"?"":"settling: "}${a.detail}`}))}}u(se,"siteHealth");const I=[{value:"always",label:"Always on",description:u(()=>"Codex implements and reviews. Work pauses when it runs out.","description")},{value:"auto",label:"Auto",description:u(e=>`Codex implements and reviews, and ${e} takes over while it's out.`,"description")},{value:"off",label:"Off",description:u(e=>`${e} implements and reviews.`,"description")}],ie=u((e,t,s)=>({usable:t.usable,tag:t.usable?null:O(t.reason),share:e,choices:I.map(({value:o,label:n})=>({value:o,label:n,on:o===e})),description:t.usable?I.find(o=>o.value===e).description(P[s]):t.words,fix:t.fix}),"codexView"),P={opus:"Opus",sonnet:"Sonnet",auto:"Claude"},U=[{value:"opus",label:"Opus",description:"Always Opus. Auto would give small cards to Sonnet."},{value:"auto",label:"Auto",description:"Auto: small cards get Sonnet, the rest get Opus."},{value:"sonnet",label:"Sonnet",description:"Always Sonnet. Auto would give large cards to Opus."}],ue=u(e=>({model:e,builder:P[e],choices:U.map(({value:t,label:s})=>({value:t,label:s,on:t===e})),description:U.find(t=>t.value===e).description}),"claudeView");function me({now:e,st:t,poll:s,sidebar:o,rows:n,links:r=new Map,code:d={},host:c="",rules:a=[],remembered:m=new Map,integrations:i=[],codexShare:h="off",claudeModel:p="auto",codex:v=ee({installed:!1}),awake:C=null}){const b=new Set(o.sites.filter(l=>l.board).map(l=>l.route)),M=d.running&&d.running===d.onDisk?` \xB7 runs the code in its files (${d.onDisk})`:"",V=u(l=>({route:l.route,title:l.title,icon:Q(l.route,l),href:`/${encodeURIComponent(l.route)}/`,...se(l,r.get(l.route)),remembered:m.get(l.route)||[],restart:b.has(l.route)&&l.restart===!0}),"site"),z=[["Boards",n.filter(l=>b.has(l.route))],["Pages",n.filter(l=>!b.has(l.route))]].filter(([,l])=>l.length).map(([l,W])=>({label:l,sites:W.map(V)}));return{hub:{tone:o.hub.tone,word:o.hub.word,detail:`${o.hub.detail}${M}`},awake:C&&$(C,e,S),groups:z,integrations:i,usage:o.usage,pause:o.pause,codex:ie(h,v,p),claude:ue(p),empty:n.length===0,polling:{...N(t,s,e),summary:_(t.settings)},notifications:{on:t.settings.notifications.on,text:o.notifications.text,device:A(t,e)},hold:t.hold&&t.hold.why!=="off"?{text:`Notifications ${D(t.hold,e)}`,end:t.hold.why==="snoozed"}:null,rules:a.length?g(a.length,"rule"):"None yet",sites:g(n.length,"site"),host:c,total:o.total}}u(me,"hubView");const ge="That snooze ends in the past or more than 30 days on. Pick another time.",ye="That keep awake ends in the past or more than 30 days on. Pick another time.",L="until you end it",S="until you turn it off",$=u((e,t,s)=>e===x?s:`until ${w(e,t)}`,"untilWords"),q=u((e,t)=>Z.map(([s,o])=>({value:s,label:o,until:$(X(e,s),e,t)})),"durationChoices"),le=u((e,t)=>e.on?J(e.snoozed_until,t):null,"snoozedUntil");function $e({now:e,st:t,rules:s=[],route:o={atMac:!1,desktops:0},linear:n={set:!1},awake:r={until:null,unsupported:null}}){const d=t.settings,c=d.notifications,[a,m]=[d.windows[0],d.windows.at(-1)],i=le(c,e),h=t.count?` The ${t.count} queued push${t.count===1?" is":"es are"} dropped now.`:"",p=i&&`Snoozed ${$(i,e,L)}`;return{snoozed:i?{title:p,summary:i===x?"Snoozed \xB7 forever":p}:null,device:A(t,e),on:{on:c.on,sub:c.on?"On. Turn off to drop every push, urgent ones too.":"Off. Every push is dropped, urgent ones too. Needs you and the badge still update.",confirm:`Turn off notifications? Every push is dropped until you turn them back on, urgent ones too.${h} Needs you and the badge still update.`},presence:c.on?o.atMac?o.desktops?{title:"At the host",sub:"Pushes go to the host only. Prompts, faults and urgent pushes still reach the phone."}:{title:"At the host",sub:"No notifications on the host, so pushes still go to the phone."}:{title:"Away from the host",sub:"Pushes go to every device. At the host means unlocked and used in the last 3 minutes."}:null,snooze:c.on&&!i?{choices:q(e,L)}:null,awake:{unsupported:r.unsupported,on:r.until?{title:`Keeping awake ${$(r.until,e,S)}`}:null,choices:r.unsupported||r.until?null:q(e,S),summary:r.unsupported||(r.until?r.until===x?"On \xB7 forever":`On until ${w(r.until,e)}`:"Off")},weekends:{on:c.weekends_off,sub:`Pushes wait from Friday ${f(m.from)} until Monday ${f(a.from)}. Urgent ones still go.`},polling:_(d),rules:s,linear:re(n)}}u($e,"settingsView");const k={url:"https://linear.app/settings/account/security",label:"Linear \u203A Settings \u203A Security & access \u203A Personal API keys"};function re(e){const t=e.workspace?`${e.workspace} workspace`:"";return e.set?e.refused?{set:!0,refused:!0,title:t||"Linear key",down:"Doesn\u2019t work",sub:`Linear refused the key. Make a new one in ${k.label} and paste it here.`,keys:k,summary:t?`Key doesn\u2019t work \xB7 ${t}`:"Key doesn\u2019t work"}:{set:!0,title:t,sub:"Key \u2022\u2022\u2022\u2022\u2022\u2022\u2022\u2022",summary:`Key set \xB7 ${t}`}:{set:!1,title:"No key",sub:`Make a personal API key in ${k.label}, then paste it here.`,keys:k,summary:"No key"}}u(re,"linearWords");const ae=[5,10,15,30,60,120],de=u(e=>Math.round(100-Math.log(e/5)/Math.log(24)*70),"shade"),H=u((e,t)=>e.windows.length===1?"All day":`${f(e.windows[t].from)} \u2013 ${f(e.windows[(t+1)%e.windows.length].from)}`,"windowRange");function ke({now:e,st:t,poll:s,select:o=null}){const n=t.settings,r=n.windows.findIndex(i=>i.from===o),d=r>=0?r:n.windows.findIndex(i=>i.from===K(e,n).from),c=n.windows.length===1,[a,m]=[n.windows[0],n.windows.at(-1)];return{select:n.windows[d].from,now:N(t,s,e),day:{segments:G(n).map(i=>{const h=n.windows[i.window];return{w:h.from,left:y(i.start),width:y(i.end-i.start),shade:`${de(h.every_minutes)}%`,held:i.held,label:i.end-i.start>=110?E(h.every_minutes).replace(" min","m").replace(" h","h"):"",aria:`${H(n,i.window)}, every ${E(h.every_minutes)}${i.held?", pushes held":""}`}}),pins:n.wakes.map(i=>({left:y(te(i)),title:`Wake ${f(i)}`})),now:y(e.getHours()*60+e.getMinutes()),held:R(n),wakes:n.wakes.length>0},windows:n.windows.map((i,h)=>({from:i.from,range:H(n,h),ends:c?"\u2014":f(n.windows[(h+1)%n.windows.length].from),every:[...new Set([...ae,i.every_minutes])].sort((p,v)=>p-v).map(p=>({minutes:p,label:E(p),on:p===i.every_minutes})),checks:`${g(T(n,h),"check")} in this window`,split:!!j(n,h),remove:!c})),hold:{on:n.hold_overnight,sub:c?"With one window there is no overnight to hold.":`Pushes queued after ${f(m.from)} wait until ${f(a.from)}. GitHub checks still run.`},wakes:n.wakes.map(i=>({value:i,label:f(i)}))}}u(ke,"pollingView");function ve(e){return e?e.ok?{sent:e.sent,count:e.count,ok:!0,message:`Sent ${e.count} queued push${e.count===1?"":"es"}.`}:{sent:e.sent,count:e.count,ok:!1,message:"Could not send the queue. It stays queued for the next check."}:{sent:0,count:0,ok:!0,message:"Nothing was queued."}}u(ve,"flushView");export{ye as AWAKE_REFUSED,ge as SNOOZE_REFUSED,O as capital,q as durationChoices,E as every,ve as flushView,me as hubView,A as nowLine,g as plural,oe as pollLine,_ as pollingSummary,ke as pollingView,$e as settingsView,se as siteHealth};
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@lovek4d/kickflip",
3
- "version": "0.1.0",
3
+ "version": "0.3.1",
4
4
  "description": "Follow and steer Claude Code sessions from your phone.",
5
5
  "license": "SEE LICENSE IN LICENSE",
6
6
  "type": "module",
package/shared/colors.css CHANGED
@@ -1,13 +1,15 @@
1
1
  /* The one colour set for the hub and every board (docs/adr/0033-one-colour-constants-file-shared-by-the-hub-and-every-board.md).
2
- board/server.mjs serves it as colors.css and hub/hub.mjs as /_hub/colors.css, linked before each page's own CSS. */
2
+ board/server.mjs serves it as colors.css and hub/hub.mjs as /_hub/colors.css, linked before each page's own CSS.
3
+ --accent is the one human blue: new messages, links, buttons, focus rings, the owner's bubble (--accent-soft) and
4
+ planner lines; --on-accent is the label on an accent fill (docs/adr/0169-one-human-blue.md). */
3
5
  :root {
4
6
  --bg: #fbfbfc; --panel: #ffffff; --line: #e6e6ea; --text: #1b1b1f; --muted: #6b6b76;
5
- --accent: #5e6ad2; --accent-soft: #eef0ff; --ok: #1f9d55; --warn: #c27c0e; --bad: #d1435b;
6
- --chip: #f1f1f4; --band: #f2f2f5; --love: #eef6ff; --claude: #fdf3e4;
7
+ --accent: #2b6cb0; --accent-soft: #eef6ff; --on-accent: #ffffff; --ok: #1f9d55; --warn: #c27c0e; --bad: #d1435b;
8
+ --chip: #f1f1f4; --band: #f2f2f5; --claude: #fdf3e4;
7
9
  --color-quiet: var(--muted); --color-info: var(--accent); --color-notice: var(--warn); --color-danger: var(--bad); --color-success: var(--ok);
8
10
  }
9
11
  @media (prefers-color-scheme: dark) {
10
12
  :root { --bg: #111114; --panel: #18181c; --line: #2a2a31; --text: #ececf1; --muted: #9a9aa6;
11
- --accent: #8c95f0; --accent-soft: #23253d; --ok: #3ccf7f; --warn: #e3a33a; --bad: #f0687f;
12
- --chip: #24242b; --band: #202026; --love: #1b2533; --claude: #2d2416; }
13
+ --accent: #6fa8e8; --accent-soft: #1b2533; --on-accent: #0b0b0e; --ok: #3ccf7f; --warn: #e3a33a; --bad: #f0687f;
14
+ --chip: #24242b; --band: #202026; --claude: #2d2416; }
13
15
  }
@@ -0,0 +1,54 @@
1
+ var g=Object.defineProperty;var r=(e,s)=>g(e,"name",{value:s,configurable:!0});import n from"node:fs";import a from"node:path";import{PR_VIEW_FIELDS as p}from"../hub/pr-report.mjs";const i=r((e,s,t,c)=>{const o=a.join(e,s,`${t}.json`);n.mkdirSync(a.dirname(o),{recursive:!0}),n.writeFileSync(o,JSON.stringify(c))},"write"),S=r((e,s,t)=>i(e,"pr",s,t),"writePR"),j=r((e,s,t)=>i(e,"issue-comments",s,t),"writeIssueComments"),v=r((e,s,t)=>i(e,"review-comments",s,t),"writeReviewComments"),y=r((e,s,t)=>i(e,"reviews",s,t),"writeReviews"),w=r((e,s,t)=>i(e,"merge",s,t),"writeMerge"),x=r((e,s)=>i(e,"","user",s),"writeUser"),O=r((e,s)=>i(e,"","accounts",s),"writeAccounts"),l=r((e,s)=>{try{return n.readFileSync(a.join(e,s),"utf8").trim().split(`
2
+ `).filter(Boolean).map(JSON.parse)}catch{return[]}},"jsonl"),E=r(e=>l(e,"calls.jsonl"),"calls"),N=r(e=>l(e,"tokens.jsonl"),"tokens");function R({head:e="abc1234567",failing:s=[],pending:t=0,...c}={}){return{state:"OPEN",isDraft:!1,reviewDecision:"REVIEW_REQUIRED",mergeable:"MERGEABLE",mergeStateStatus:"BLOCKED",headRefOid:e,statusCheckRollup:[...s.map(o=>({__typename:"CheckRun",name:o,status:"COMPLETED",conclusion:"FAILURE"})),...Array.from({length:t},(o,u)=>({__typename:"CheckRun",name:`ci-${u}`,status:"IN_PROGRESS",conclusion:""}))],...c}}r(R,"prView");function k(e){const s=a.join(e,"bin");return n.mkdirSync(s,{recursive:!0}),n.writeFileSync(a.join(s,"gh"),`#!${process.execPath}
3
+ const fs = require('node:fs');
4
+ const path = require('node:path');
5
+ const dir = ${JSON.stringify(e)};
6
+ const args = process.argv.slice(2);
7
+ fs.appendFileSync(path.join(dir, 'calls.jsonl'), JSON.stringify(args) + '\\n');
8
+ fs.appendFileSync(path.join(dir, 'tokens.jsonl'), JSON.stringify({ args, token: process.env.GH_TOKEN || null }) + '\\n');
9
+ const fail = () => { console.error('no fixture for: ' + args.join(' ')); process.exit(1); };
10
+ if (args.length === 4 && args[0] === 'auth' && args[1] === 'token' && args[2] === '-u') {
11
+ let accounts = {};
12
+ try { accounts = JSON.parse(fs.readFileSync(path.join(dir, 'accounts.json'), 'utf8')); } catch {}
13
+ if (!accounts[args[3]]) { console.error('no oauth token found for github.com account ' + args[3]); process.exit(1); }
14
+ console.log(accounts[args[3]]);
15
+ process.exit(0);
16
+ }
17
+ if (args[0] === 'issue' && args.includes('--repo')) {
18
+ const repo = args[args.indexOf('--repo') + 1];
19
+ if (args[1] === 'create') {
20
+ const n = fs.readFileSync(path.join(dir, 'calls.jsonl'), 'utf8').split('\\n').filter((l) => l.startsWith('["issue","create"')).length;
21
+ console.log('https://github.com/' + repo + '/issues/' + n);
22
+ } else if (args[1] === 'view') console.log(JSON.stringify(args[6] === 'number,url' ? { number: Number(args[2]), url: 'https://github.com/' + repo + '/issues/' + args[2] } : { state: 'OPEN', assignees: [] }));
23
+ else if (args[1] !== 'close') fail();
24
+ process.exit(0);
25
+ }
26
+ if (args.length === 6 && args[0] === 'pr' && args[1] === 'merge' && /^\\d+$/.test(args[2]) && args[3] === '--repo' && args[5] === '--squash') {
27
+ let refused = null;
28
+ try { refused = JSON.parse(fs.readFileSync(path.join(dir, 'merge', args[2] + '.json'), 'utf8')).error; } catch {}
29
+ if (refused) { console.error(refused); process.exit(1); }
30
+ process.exit(0);
31
+ }
32
+ let file;
33
+ let list = false;
34
+ if (args.length === 7 && args[0] === 'pr' && args[1] === 'view' && /^\\d+$/.test(args[2]) && args[3] === '--repo' && args[5] === '--json' && args[6] === ${JSON.stringify(p)}) file = path.join(dir, 'pr', args[2] + '.json');
35
+ else if (args.length === 2 && args[0] === 'api' && args[1] === 'user') file = path.join(dir, 'user.json');
36
+ else if (args.length === 4 && args[0] === 'api' && args[1] === '--paginate' && args[2] === '--slurp') {
37
+ const match = /^repos\\/[^/]+\\/[^/]+\\/(issues|pulls)\\/(\\d+)\\/(comments\\?since=.+|reviews)$/.exec(args[3]);
38
+ if (!match || (match[1] === 'issues' && match[3] === 'reviews')) process.exit(1);
39
+ file = path.join(dir, match[3] === 'reviews' ? 'reviews' : match[1] === 'issues' ? 'issue-comments' : 'review-comments', match[2] + '.json');
40
+ list = true;
41
+ } else process.exit(1);
42
+ let value;
43
+ try { value = fs.readFileSync(file, 'utf8'); } catch { if (!list) fail(); value = '[]'; }
44
+ if (list) {
45
+ value = JSON.parse(value);
46
+ if (value.fail) fail();
47
+ const pages = [];
48
+ for (let i = 0; i < value.length; i += 2) pages.push(value.slice(i, i + 2));
49
+ console.log(JSON.stringify(pages));
50
+ } else {
51
+ try { if (JSON.parse(value).fail) fail(); } catch {}
52
+ console.log(value);
53
+ }
54
+ `,{mode:493}),s}r(k,"fakeGh");export{E as calls,k as fakeGh,R as prView,N as tokens,O as writeAccounts,j as writeIssueComments,w as writeMerge,S as writePR,v as writeReviewComments,y as writeReviews,x as writeUser};
@@ -1,33 +1,33 @@
1
- var L=Object.defineProperty;var n=(i,d)=>L(i,"name",{value:d,configurable:!0});import u from"node:fs";import A from"node:os";import f from"node:path";import{spawnSync as T}from"node:child_process";const $="local.claude.kickflip-hub",b="local.claude.web-ts-hub",p="kickflip-hub.service";function P(i,d,{env:y=process.env,timeout:o=1e4}={}){const e=T(i,d,{encoding:"utf8",env:y,timeout:o});return{status:e.status??1,stdout:e.stdout||"",error:(e.error?.message||e.stderr||`exit ${e.status}`).trim().split(`
2
- `)[0]}}n(P,"runCommand");function U({platform:i=process.platform,home:d=A.homedir(),uid:y=process.getuid?.(),env:o=process.env,run:e=P}={}){const a=n((r,s,l={})=>{try{return e(r,s,{env:o,...l})}catch(c){return{status:1,stdout:"",error:c.message}}},"exec");if(i==="darwin")return x({home:d,uid:y,exec:a});if(i==="linux")return O({home:d,uid:y,exec:a});const t=`kickflip keeps the hub running with launchd on macOS or systemd on Linux, not on ${i}`;return{manager:null,item:"hub service",file:null,id:null,restartCommand:"node hub/hub.mjs from the kickflip folder",definition:n(()=>null,"definition"),install:n(async()=>{throw new Error(t)},"install"),restart:n(()=>t,"restart"),remove:n(()=>{},"remove"),status:n(()=>({installed:!1,running:!1,detail:t}),"status"),notes:n(()=>[],"notes")}}n(U,"hubService");const E=12e4;function x({home:i,uid:d,exec:y}){const o=f.join(i,"Library","LaunchAgents"),e=`gui/${d}`,a=f.join(o,`${$}.plist`),t=f.join(o,`${b}.plist`),r=n((s,l)=>y("launchctl",s,l),"launchctl");return{manager:"launchd",item:"hub login item",file:a,id:`${e}/${$}`,restartCommand:`launchctl kickstart -k gui/$(id -u)/${$}`,definition({node:s,entry:l,state:c,port:g,log:h}){const m=n(k=>String(k).replaceAll("&","&amp;").replaceAll("<","&lt;").replaceAll(">","&gt;"),"xml");return`<?xml version="1.0" encoding="UTF-8"?>
1
+ var S=Object.defineProperty;var n=(i,u)=>S(i,"name",{value:u,configurable:!0});import p from"node:fs";import v from"node:os";import $ from"node:path";import{spawnSync as A}from"node:child_process";const d="local.claude.kickflip-hub",m="kickflip-hub.service";function E(i,u,{env:g=process.env,timeout:o=1e4}={}){const r=A(i,u,{encoding:"utf8",env:g,timeout:o});return{status:r.status??1,stdout:r.stdout||"",error:(r.error?.message||r.stderr||`exit ${r.status}`).trim().split(`
2
+ `)[0]}}n(E,"runCommand");function D({platform:i=process.platform,home:u=v.homedir(),uid:g=process.getuid?.(),env:o=process.env,run:r=E}={}){const a=n((t,s,c={})=>{try{return r(t,s,{env:o,...c})}catch(l){return{status:1,stdout:"",error:l.message}}},"exec");if(i==="darwin")return L({home:u,uid:g,exec:a});if(i==="linux")return T({home:u,uid:g,exec:a});const e=`kickflip keeps the hub running with launchd on macOS or systemd on Linux, not on ${i}`;return{manager:null,item:"hub service",file:null,id:null,restartCommand:"node hub/hub.mjs from the kickflip folder",definition:n(()=>null,"definition"),install:n(async()=>{throw new Error(e)},"install"),restart:n(()=>e,"restart"),remove:n(()=>{},"remove"),status:n(()=>({installed:!1,running:!1,detail:e}),"status"),notes:n(()=>[],"notes")}}n(D,"hubService");const b=12e4;function L({home:i,uid:u,exec:g}){const o=$.join(i,"Library","LaunchAgents"),r=`gui/${u}`,a=$.join(o,`${d}.plist`),e=n((t,s)=>g("launchctl",t,s),"launchctl");return{manager:"launchd",item:"hub login item",file:a,id:`${r}/${d}`,restartCommand:`launchctl kickstart -k gui/$(id -u)/${d}`,definition({node:t,entry:s,state:c,port:l,log:h}){const y=n(w=>String(w).replaceAll("&","&amp;").replaceAll("<","&lt;").replaceAll(">","&gt;"),"xml");return`<?xml version="1.0" encoding="UTF-8"?>
3
3
  <!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
4
4
  <plist version="1.0">
5
5
  <dict>
6
- <key>Label</key><string>${$}</string>
6
+ <key>Label</key><string>${d}</string>
7
7
  <key>ProgramArguments</key>
8
- <array><string>${m(s)}</string><string>${m(l)}</string></array>
9
- <key>WorkingDirectory</key><string>${m(c)}</string>
8
+ <array><string>${y(t)}</string><string>${y(s)}</string></array>
9
+ <key>WorkingDirectory</key><string>${y(c)}</string>
10
10
  <key>EnvironmentVariables</key>
11
- <dict><key>PATH</key><string>${m(f.dirname(s))}:/opt/homebrew/bin:/usr/local/bin:/usr/bin:/bin:/usr/sbin:/sbin</string><key>KICKFLIP_HUB_PORT</key><string>${g}</string></dict>
11
+ <dict><key>PATH</key><string>${y($.dirname(t))}:/opt/homebrew/bin:/usr/local/bin:/usr/bin:/bin:/usr/sbin:/sbin</string><key>KICKFLIP_HUB_PORT</key><string>${l}</string></dict>
12
12
  <key>RunAtLoad</key><true/>
13
13
  <key>KeepAlive</key><true/>
14
- <key>StandardOutPath</key><string>${m(h)}</string>
15
- <key>StandardErrorPath</key><string>${m(h)}</string>
14
+ <key>StandardOutPath</key><string>${y(h)}</string>
15
+ <key>StandardErrorPath</key><string>${y(h)}</string>
16
16
  </dict>
17
17
  </plist>
18
- `},async install(s,{wait:l=n(g=>new Promise(h=>setTimeout(h,g)),"wait"),dryRun:c=!1}={}){let g=null;try{g=u.readFileSync(t,"utf8")}catch(v){if(v.code!=="ENOENT")throw v}const h=[];if(c)return g!==null&&h.push(`would stop ${e}/${b} and remove ${t}`),[...h,`would write ${a} and start ${e}/${$}`];u.mkdirSync(o,{recursive:!0}),u.writeFileSync(a,s),g!==null&&(r(["bootout",`${e}/${b}`]),u.unlinkSync(t),h.push(`stopped ${e}/${b} and removed ${t}`)),r(["bootout",`${e}/${$}`]),await l(500);const m=r(["bootstrap",e,a]);if(m.status===0)return[...h,`started ${e}/${$} from ${a}`];if(g===null)throw new Error(m.error);u.writeFileSync(t,g),u.rmSync(a,{force:!0});const k=r(["bootstrap",e,t]);throw new Error(`${m.error}; ${k.status===0?`restarted ${e}/${b} from ${t}`:`could not restart ${e}/${b} either (${k.error})`}`)},restart(){const s=r(["kickstart","-k",`${e}/${$}`],{timeout:E});return s.status===0?null:s.error},remove(){r(["bootout",`${e}/${$}`]),u.rmSync(a,{force:!0})},status(){const s=r(["print",`${e}/${$}`]),l=u.existsSync(a),c=l&&s.status===0&&/\bstate = running\b/.test(s.stdout||"");return{installed:l,running:c,detail:c?"running":l?"not running":"not installed"}},notes:n(()=>[],"notes")}}n(x,"launchd");const w=n(i=>String(i).replaceAll("%","%%").replaceAll("$","$$$$").replace(/[\r\n]/g," "),"unitValue"),S=n(i=>`"${w(i).replaceAll("\\","\\\\").replaceAll('"','\\"')}"`,"execWord");function O({home:i,uid:d,exec:y}){const o=f.join(i,".config","systemd","user",p),e=n((t,r)=>y("systemctl",["--user",...t],r),"systemctl"),a=n(t=>{const r=e(t);if(r.status!==0)throw new Error(`systemctl --user ${t.join(" ")}: ${r.error}`)},"step");return{manager:"systemd",item:"hub service",file:o,id:p,restartCommand:`systemctl --user restart ${p}`,definition({node:t,entry:r,state:s,port:l,log:c}){return`[Unit]
18
+ `},async install(t,{wait:s=n(l=>new Promise(h=>setTimeout(h,l)),"wait"),dryRun:c=!1}={}){if(c)return[`would write ${a} and start ${r}/${d}`];p.mkdirSync(o,{recursive:!0}),p.writeFileSync(a,t),e(["bootout",`${r}/${d}`]),await s(500);const l=e(["bootstrap",r,a]);if(l.status===0)return[`started ${r}/${d} from ${a}`];throw new Error(l.error)},restart(){const t=e(["kickstart","-k",`${r}/${d}`],{timeout:b});return t.status===0?null:t.error},remove(){e(["bootout",`${r}/${d}`]),p.rmSync(a,{force:!0})},status(){const t=e(["print",`${r}/${d}`]),s=p.existsSync(a),c=s&&t.status===0&&/\bstate = running\b/.test(t.stdout||"");return{installed:s,running:c,detail:c?"running":s?"not running":"not installed"}},notes:n(()=>[],"notes")}}n(L,"launchd");const k=n(i=>String(i).replaceAll("%","%%").replaceAll("$","$$$$").replace(/[\r\n]/g," "),"unitValue"),f=n(i=>`"${k(i).replaceAll("\\","\\\\").replaceAll('"','\\"')}"`,"execWord");function T({home:i,uid:u,exec:g}){const o=$.join(i,".config","systemd","user",m),r=n((e,t)=>g("systemctl",["--user",...e],t),"systemctl"),a=n(e=>{const t=r(e);if(t.status!==0)throw new Error(`systemctl --user ${e.join(" ")}: ${t.error}`)},"step");return{manager:"systemd",item:"hub service",file:o,id:m,restartCommand:`systemctl --user restart ${m}`,definition({node:e,entry:t,state:s,port:c,log:l}){return`[Unit]
19
19
  Description=kickflip hub
20
20
 
21
21
  [Service]
22
- ExecStart=${S(t)} ${S(r)}
23
- WorkingDirectory=${w(s)}
24
- Environment=${S(`PATH=${f.dirname(t)}:/usr/local/bin:/usr/bin:/bin:/usr/sbin:/sbin`)}
25
- Environment=KICKFLIP_HUB_PORT=${l}
22
+ ExecStart=${f(e)} ${f(t)}
23
+ WorkingDirectory=${k(s)}
24
+ Environment=${f(`PATH=${$.dirname(e)}:/usr/local/bin:/usr/bin:/bin:/usr/sbin:/sbin`)}
25
+ Environment=KICKFLIP_HUB_PORT=${c}
26
26
  Restart=always
27
27
  RestartSec=2
28
- StandardOutput=append:${w(c)}
29
- StandardError=append:${w(c)}
28
+ StandardOutput=append:${k(l)}
29
+ StandardError=append:${k(l)}
30
30
 
31
31
  [Install]
32
32
  WantedBy=default.target
33
- `},async install(t,{dryRun:r=!1}={}){return r?[`would write ${o}, enable and start ${p}`]:(u.mkdirSync(f.dirname(o),{recursive:!0}),u.writeFileSync(o,t),a(["daemon-reload"]),a(["enable",p]),a(["restart",p]),[`enabled and started ${p} from ${o}`])},restart(){const t=e(["restart",p],{timeout:E});return t.status===0?null:t.error},remove(){e(["disable","--now",p]),u.rmSync(o,{force:!0}),e(["daemon-reload"])},status(){const t=e(["is-active",p]),r=u.existsSync(o),s=r&&t.status===0&&(t.stdout||"").trim()==="active";return{installed:r,running:s,detail:s?"running":r?"not running":"not installed"}},notes(){const t=y("loginctl",["show-user",String(d),"--property=Linger"]),r=t.status===0&&/^Linger=yes$/m.test(t.stdout||"");return[{item:"hub without a login",state:r?"ok":"optional",detail:r?"the user lingers, so the hub runs with nobody logged in":"the hub stops when your last login session ends",fix:'sudo loginctl enable-linger "$USER"'},{item:"macOS-only parts",state:"note",detail:"At the host always reads away, since presence comes from macOS ioreg, so pushes reach every device"}]}}}n(O,"systemd");export{$ as LOGIN_LABEL,p as UNIT,U as hubService,P as runCommand};
33
+ `},async install(e,{dryRun:t=!1}={}){return t?[`would write ${o}, enable and start ${m}`]:(p.mkdirSync($.dirname(o),{recursive:!0}),p.writeFileSync(o,e),a(["daemon-reload"]),a(["enable",m]),a(["restart",m]),[`enabled and started ${m} from ${o}`])},restart(){const e=r(["restart",m],{timeout:b});return e.status===0?null:e.error},remove(){r(["disable","--now",m]),p.rmSync(o,{force:!0}),r(["daemon-reload"])},status(){const e=r(["is-active",m]),t=p.existsSync(o),s=t&&e.status===0&&(e.stdout||"").trim()==="active";return{installed:t,running:s,detail:s?"running":t?"not running":"not installed"}},notes(){const e=g("loginctl",["show-user",String(u),"--property=Linger"]),t=e.status===0&&/^Linger=yes$/m.test(e.stdout||"");return[{item:"hub without a login",state:t?"ok":"optional",detail:t?"the user lingers, so the hub runs with nobody logged in":"the hub stops when your last login session ends",fix:'sudo loginctl enable-linger "$USER"'},{item:"macOS-only parts",state:"note",detail:"At the host always reads away, since presence comes from macOS ioreg, so pushes reach every device"}]}}}n(T,"systemd");export{d as LOGIN_LABEL,m as UNIT,D as hubService,E as runCommand};
@@ -0,0 +1,2 @@
1
+ var g=Object.defineProperty;var l=(t,e)=>g(t,"name",{value:e,configurable:!0});import k from"node:fs";import c from"node:os";import a from"node:path";const o="mattpocock/skills",d=["improve-codebase-architecture","codebase-design","grilling","prototype"],m=l((t,e=c.homedir())=>a.join(e,".claude","skills",t,"SKILL.md"),"skillFile"),f=l((t=c.homedir())=>d.filter(e=>!k.existsSync(m(e,t))),"missingSkills"),v=l(t=>["--yes","skills","add",o,"-g","-y","-a","claude-code","--skill",...t],"addArgv"),w=l(t=>`npx skills add ${o} -g -y -a claude-code --skill ${t.join(" ")}`,"addCommand"),E=l(t=>`npx skills update ${t.join(" ")} -g -y`,"updateCommand");function x(t){let e;try{e=JSON.parse(k.readFileSync(a.join(t,".agents",".skill-lock.json"),"utf8"))}catch{return{}}return Object.fromEntries(d.flatMap(r=>{const s=e?.skills?.[r];return s?.source===o&&s.skillPath&&s.skillFolderHash?[[r,{folder:a.posix.dirname(s.skillPath),hash:s.skillFolderHash}]]:[]}))}l(x,"lockedSkills");function F({home:t=c.homedir(),run:e,gh:r=!0}={}){const s=f(t),n=x(t),h=d.filter(i=>!s.includes(i)&&n[i]);if(!r||!h.length)return{missing:s,stale:[],checked:!1};const p=e("gh",["api",`repos/${o}/git/trees/HEAD?recursive=1`,"--jq",'.tree[] | select(.type == "tree") | .path + " " + .sha']);if(p.status!==0)return{missing:s,stale:[],checked:!1};const u=new Map(p.stdout.split(`
2
+ `).filter(Boolean).map(i=>i.split(" ")));return{missing:s,stale:h.filter(i=>u.has(n[i].folder)&&u.get(n[i].folder)!==n[i].hash),checked:!0}}l(F,"skillsState");const S=l(t=>t.length<3?t.join(" and "):`${t.slice(0,-1).join(", ")} and ${t.at(-1)}`,"list");function I(t=c.homedir()){const e=f(t);return e.length?`Architecture reviews wait on ${S(e)} from ${o}, which ${e.length===1?"isn\u2019t":"aren\u2019t"} installed. Run kickflip doctor --fix on the host to add ${e.length===1?"it":"them"}.`:null}l(I,"reviewWaits");export{d as REVIEW_SKILLS,o as SKILLS_SOURCE,v as addArgv,w as addCommand,f as missingSkills,I as reviewWaits,m as skillFile,F as skillsState,E as updateCommand};
@@ -1 +1 @@
1
- var m=Object.defineProperty;var n=(e,t)=>m(e,"name",{value:t,configurable:!0});import g from"node:os";import{usageReader as D}from"../hub/usage.mjs";import{readSettingsFile as f,writeSettingsFile as x,readCodexShare as S}from"../hub/push.mjs";import{codexStatusReader as N}from"./codex.mjs";const G=n(e=>Number.isInteger(e)&&e>=50&&e<=100&&e%5===0,"validPauseAt"),d=n(e=>Number(e),"timeOf"),O=["five_hour","seven_day"];function A({claude:e,codex:t,pauseAt:s=90,keepGoing:r,now:a=Date.now()}={}){const p=G(s)?s:90,l=n(c=>p===100?null:O.find(o=>{const i=c?.[o];return i?.state==="live"&&Number.isFinite(i.used_percentage)&&i.used_percentage>=p&&(!i.resets_at||Date.parse(i.resets_at)>d(a))})||null,"past"),w=n((c,o)=>({window:o,used:o?c[o].used_percentage:null,resets_at:o?c[o].resets_at:null}),"details"),u=l(e),h=w(e,u),k=u&&r?.window===u&&Math.abs(Date.parse(r.until)-Date.parse(h.resets_at))<=3e5&&d(a)<Date.parse(r.until),_=l(t);return{pause_at:p,state:u?k?"going":"paused":"normal",...h,codex:{past:!!_,...w(t,_)}}}n(A,"usageGate");const b=new Map;function E({home:e=g.homedir(),now:t=Date.now()}={}){let s=b.get(e);s||(s={now:d(t)},s.usage=D({home:e,now:n(()=>s.now,"now")}),s.codex=N({home:e,now:n(()=>s.now,"now")}),b.set(e,s)),s.now=d(t);const r=f(e);return A({...s.usage({codexShare:S(s.codex(),e)}),pauseAt:r.pauseAt,keepGoing:r.keepGoing,now:t})}n(E,"readUsageGate");function M(e,{home:t=g.homedir()}={}){if(!/^(?:50|55|60|65|70|75|80|85|90|95|100)$/.test(String(e)))throw Object.assign(new Error("pause-at takes 50..100 in steps of 5"),{status:400});x({...f(t),pauseAt:Number(e)},t)}n(M,"setPauseAt");function C(e,{home:t=g.homedir(),now:s=Date.now()}={}){if(!["on","off"].includes(e))throw Object.assign(new Error("keep-going takes on or off"),{status:400});const r=f(t);if(e==="on"){const a=E({home:t,now:s});if(a.state!=="paused")throw Object.assign(new Error("Nothing is paused."),{status:409});r.keepGoing={window:a.window,until:a.resets_at}}else delete r.keepGoing;x(r,t)}n(C,"setKeepGoing");const K=n(e=>{try{return["github-change","check-github","card-unblocked"].includes(JSON.parse(e).kind)}catch{return!1}},"heldUsageEvent"),P=n(e=>`Work is paused: Claude's ${e.window==="five_hour"?"5-hour":"weekly"} usage is at ${Math.round(e.used)}%. Resumes at ${e.resets_at}. The owner can tap Keep going on the Hub page.`,"pauseReason");export{K as heldUsageEvent,P as pauseReason,E as readUsageGate,C as setKeepGoing,M as setPauseAt,A as usageGate,G as validPauseAt};
1
+ var S=Object.defineProperty;var o=(e,t)=>S(e,"name",{value:t,configurable:!0});import g from"node:os";import{usageReader as $}from"../hub/usage.mjs";import{readSettingsFile as x,writeSettingsFile as m,readCodexShare as A,readClaudeModel as D}from"../hub/push.mjs";import{codexStatus as N,codexStatusReader as M,effectiveCodexShare as O}from"./codex.mjs";const E=o(e=>Number.isInteger(e)&&e>=50&&e<=100&&e%5===0,"validPauseAt"),h=o(e=>Number(e),"timeOf"),G=["five_hour","seven_day"];function P({claude:e,codex:t,pauseAt:s=90,keepGoing:n,now:a=Date.now()}={}){const i=E(s)?s:90,r=o(f=>i===100?null:G.find(d=>{const c=f?.[d];return c?.state==="live"&&Number.isFinite(c.used_percentage)&&c.used_percentage>=i&&(!c.resets_at||Date.parse(c.resets_at)>h(a))})||null,"past"),l=o((f,d)=>({window:d,used:d?f[d].used_percentage:null,resets_at:d?f[d].resets_at:null}),"details"),u=r(e),p=l(e,u),C=u&&n?.window===u&&Math.abs(Date.parse(n.until)-Date.parse(p.resets_at))<=3e5&&h(a)<Date.parse(n.until),w=r(t);return{pause_at:i,state:u?C?"going":"paused":"normal",...p,codex:{past:!!w,...l(t,w)}}}o(P,"usageGate");const _=new Map;function k({home:e=g.homedir(),now:t=Date.now()}={}){let s=_.get(e);s||(s={now:h(t)},s.usage=$({home:e,now:o(()=>s.now,"now")}),s.codex=M({home:e,now:o(()=>s.now,"now")}),_.set(e,s)),s.now=h(t);const n=x(e);return P({...s.usage({codexShare:A(s.codex(),e)}),pauseAt:n.pauseAt,keepGoing:n.keepGoing,now:t})}o(k,"readUsageGate");function U(e,{home:t=g.homedir()}={}){if(!/^(?:50|55|60|65|70|75|80|85|90|95|100)$/.test(String(e)))throw Object.assign(new Error("pause-at takes 50..100 in steps of 5"),{status:400});m({...x(t),pauseAt:Number(e)},t)}o(U,"setPauseAt");function W(e,{home:t=g.homedir(),now:s=Date.now()}={}){if(!["on","off"].includes(e))throw Object.assign(new Error("keep-going takes on or off"),{status:400});const n=x(t);if(e==="on"){const a=k({home:t,now:s});if(a.state!=="paused")throw Object.assign(new Error("Nothing is paused."),{status:409});n.keepGoing={window:a.window,until:a.resets_at}}else delete n.keepGoing;m(n,t)}o(W,"setKeepGoing");const z=o(e=>{try{return["github-change","check-github","card-unblocked","card-pull","card-plan","todo-triage"].includes(JSON.parse(e).kind)}catch{return!1}},"heldUsageEvent");function B({home:e=g.homedir(),now:t=Date.now(),card:s={}}={}){const n=o(l=>{const{model:u,reason:p}=R(D(e),s);return{kind:"claude",model:u,reason:`${l} ${p}`}},"claude"),a=N({home:e});if(!a.usable)return n(a.words);const i=O(x(e).codexShare,a);if(i==="off")return n("Codex share is off.");const r=k({home:e,now:t});return r.codex.past?n(`Codex is at ${Math.round(r.codex.used)}%, past the ${r.pause_at}% line until ${r.codex.resets_at}.`):{kind:"codex",reason:`Codex share is ${i} and Codex is under the ${r.pause_at}% line.`}}o(B,"builderPick");const b={opus:"Opus",sonnet:"Sonnet"};function R(e,t){return e!=="auto"?{model:e,reason:`Claude model is ${b[e]}.`}:t.model?{model:t.model,reason:`Claude model is Auto and the card asks for Claude ${b[t.model]}.`}:t.size==="small"?{model:"sonnet",reason:"Claude model is Auto and the card is small."}:{model:"opus",reason:"Claude model is Auto and the card isn't small."}}o(R,"claudePick");const H=o(e=>`Work is paused: Claude's ${e.window==="five_hour"?"5-hour":"weekly"} usage is at ${Math.round(e.used)}%. Resumes at ${e.resets_at}. The owner can tap Keep going on the Hub page.`,"pauseReason");export{B as builderPick,z as heldUsageEvent,H as pauseReason,k as readUsageGate,W as setKeepGoing,U as setPauseAt,P as usageGate,E as validPauseAt};
@@ -2,7 +2,8 @@
2
2
  // relative path, e.g. <script src="claude.js"></script> or "../claude.js":
3
3
  // its own URL tells it the site's root, so links survive the /<route> prefix.
4
4
  // ClaudeSite.url('notes.html') absolute URL under the site root
5
- // await ClaudeSite.send(kind, data) sends same-origin application/json, records an event and wakes the owning session → {id}
5
+ // await ClaudeSite.send(kind, data) sends same-origin application/json, records an event and wakes the owning session → {id};
6
+ // a refusal throws with .status and .reason, the server's {error} or ''
6
7
  // await ClaudeSite.events({kind}) events newest first, each with .reply ({html} or {json}) or null
7
8
  // ClaudeSite.watch(fn, {kind}) calls fn(events) now and on change; polls every 10 s while
8
9
  // any event waits for a reply, else every 60 s; returns stop()
@@ -17,7 +18,7 @@ window.ClaudeSite = (function () {
17
18
 
18
19
  async function send(kind, data) {
19
20
  const r = await fetch(url('_claude/events'), { method: 'POST', mode: 'same-origin', headers: { 'content-type': 'application/json' }, body: JSON.stringify({ ...(data || {}), kind }) });
20
- if (!r.ok) throw new Error('send failed: ' + r.status);
21
+ if (!r.ok) throw Object.assign(new Error('send failed: ' + r.status), { status: r.status, reason: (await r.json().catch(() => null))?.error || '' });
21
22
  return r.json();
22
23
  }
23
24
 
package/bin/web-ts.mjs DELETED
@@ -1,2 +0,0 @@
1
- #!/usr/bin/env node
2
- import"./kickflip.mjs";