@lovek4d/kickflip 0.6.0 → 0.7.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (68) hide show
  1. package/CONTEXT.md +42 -26
  2. package/SKILL.md +16 -12
  3. package/bin/doctor.mjs +5 -4
  4. package/bin/event-lines.mjs +1 -0
  5. package/bin/install.mjs +3 -3
  6. package/bin/kickflip.mjs +7 -7
  7. package/bin/wait-events.mjs +2 -2
  8. package/board/agents.mjs +2 -2
  9. package/board/arch-review.mjs +1 -1
  10. package/board/bin/board.mjs +70 -42
  11. package/board/domain.mjs +7 -7
  12. package/board/finish.mjs +4 -4
  13. package/board/headless.mjs +8 -9
  14. package/board/onboarding.mjs +1 -1
  15. package/board/prototype-page.mjs +1 -1
  16. package/board/public/app-arch.js +2 -2
  17. package/board/public/app-banner.js +1 -1
  18. package/board/public/app-cards.js +36 -48
  19. package/board/public/app-lanes.js +6 -9
  20. package/board/public/app-needs.js +107 -26
  21. package/board/public/app-reader.js +52 -42
  22. package/board/public/app-threads.js +14 -10
  23. package/board/public/app.html +1 -1
  24. package/board/public/app.js +22 -12
  25. package/board/public/progress-review.js +4 -4
  26. package/board/public/prototype-kit.js +25 -11
  27. package/board/public/prototype-switcher.js +53 -33
  28. package/board/public/prototype.html +7 -4
  29. package/board/public/router.js +34 -6
  30. package/board/public/shell.js +23 -16
  31. package/board/public/sidebar.css +7 -2
  32. package/board/public/sidebar.js +3 -0
  33. package/board/public/style.css +69 -30
  34. package/board/public/ui-cards.js +61 -38
  35. package/board/public/ui-enums.js +12 -3
  36. package/board/public/ui-header.js +4 -6
  37. package/board/public/ui-hub.js +7 -4
  38. package/board/public/ui-links.js +1 -1
  39. package/board/public/ui-order.js +9 -6
  40. package/board/public/ui-page-header.js +1 -1
  41. package/board/public/ui-progress-bar.js +6 -7
  42. package/board/public/ui-questions.js +18 -3
  43. package/board/public/ui-sidebar.js +12 -5
  44. package/board/public/ui-snapshot.js +1 -1
  45. package/board/public/ui-threads.js +10 -12
  46. package/board/public/ui.js +7 -4
  47. package/board/public/view-hub.js +3 -1
  48. package/board/public/view-notes.js +1 -1
  49. package/board/public/view-questions.js +19 -3
  50. package/board/public/view-settings.js +60 -28
  51. package/board/server.mjs +19 -19
  52. package/board/stop.mjs +1 -1
  53. package/board/store.mjs +27 -25
  54. package/board/tickets.mjs +1 -1
  55. package/board/transcript.mjs +4 -2
  56. package/hooks-plugin/hooks/board-data-gate.mjs +9 -6
  57. package/hub/front-door.mjs +1 -1
  58. package/hub/home.mjs +1 -1
  59. package/hub/hub.mjs +6 -6
  60. package/hub/sessions.mjs +2 -2
  61. package/hub/try.mjs +7 -7
  62. package/hub/upgrade.mjs +1 -1
  63. package/hub/usage.mjs +3 -3
  64. package/package.json +1 -1
  65. package/shared/card-prs.mjs +1 -1
  66. package/shared/machine-gate.mjs +2 -2
  67. package/shared/tailscale.mjs +1 -1
  68. package/shared/usage-gate.mjs +1 -1
package/board/tickets.mjs CHANGED
@@ -1,3 +1,3 @@
1
- var L=Object.defineProperty;var o=(e,t)=>L(e,"name",{value:t,configurable:!0});import{runRepo as $,validateGithub as O}from"../hub/repos.mjs";import{savedClient as k}from"../hub/linear.mjs";const i=o(e=>{throw Object.assign(new Error(e),{status:400})},"refuse");function N(e){(!e||typeof e!="object"||Array.isArray(e))&&i("Tickets must name a source.");const t={internal:["source","prefix"],github:["source","account"],linear:["source","workspace","team","project"]};if(Object.hasOwn(t,e.source)||i("Ticket source must be internal, github or linear."),Object.keys(e).some(n=>!t[e.source].includes(n))&&i("Unknown ticket source field."),e.source==="internal"){const n=e.prefix===void 0||e.prefix===""?"INT":e.prefix;return(typeof n!="string"||!/^[A-Z]{2,6}$/.test(n))&&i("Prefix must be 2\u20136 uppercase letters."),{source:"internal",prefix:n}}return e.source==="github"?e.account===void 0||e.account===""?{source:"github"}:((typeof e.account!="string"||!/^[A-Za-z0-9](?:[A-Za-z0-9-]{0,38})$/.test(e.account))&&i("The gh account must be a GitHub login."),{source:"github",account:e.account}):((typeof e.workspace!="string"||!/^[a-z\d][a-z\d-]*$/i.test(e.workspace))&&i("Linear needs a workspace."),(typeof e.team!="string"||!/^[A-Z][A-Z0-9]*$/.test(e.team))&&i("Linear needs an uppercase team key."),(typeof e.project!="string"||!e.project.trim())&&i("Linear needs a project."),{source:"linear",workspace:e.workspace,team:e.team,project:e.project.trim()})}o(N,"ticketSource");const w=o(e=>{const t=e?Number(e[1]):null;return Number.isSafeInteger(t)?t:null},"numberOf"),y=o(e=>w(/^#?([1-9]\d*)$/.exec(String(e??""))),"issueNumber"),x=o(e=>e.source==="internal"?e.prefix||"INT":e.source==="linear"?e.team:null,"keyOf");function g(e,t){if(!e)return null;const n=x(e),r=String(t??"");return n?r.startsWith(`${n}-`)?w(/^([1-9]\d*)$/.exec(r.slice(n.length+1))):null:w(/^#([1-9]\d*)$/.exec(r))}o(g,"ticketIn");function C(e,t,{repo:n,key:r}={}){const u=g(e,t);if(!u)return null;if(e.source==="internal")return{id:`${x(e)}-${u}`,url:`#item=${encodeURIComponent(r)}`,source:"internal"};if(e.source==="github")return n?{id:`#${u}`,url:`https://github.com/${n}/issues/${u}`,source:"github"}:null;const l=`${e.team}-${u}`;return{id:l,url:`https://linear.app/${e.workspace}/issue/${l}`,source:"linear"}}o(C,"ticketBadge");async function j(e,t){if(!e.account)return t;const n=await t("gh",["auth","token","-u",e.account]);(n.status!==0||!n.stdout?.trim())&&i(`gh has no ${e.account} account; run gh auth login`);const r={...process.env,GH_TOKEN:n.stdout.trim()};return(u,l)=>t(u,l,{env:r})}o(j,"issueRunner");const T=o(async e=>{try{return await e()}catch(t){i(t.message)}},"linearCall");async function E(e,{ticket:t,title:n,body:r="",repo:u,next:l},{run:a=$,linearFetch:b}={}){if(e.source==="internal")return t!==void 0&&!g(e,t)&&i(`Ticket must match ${e.prefix}-<number>.`),{ticket:t??`${e.prefix}-${l}`,created:t===void 0};if(e.source==="linear"){t!==void 0&&!g(e,t)&&i(`Ticket must match ${e.team}-<number>.`);const c=k({fetch:b});if(c||i("Hub has no Linear key, so this board can\u2019t create Linear tickets; add it in Hub settings \u203A Linear, or pick another ticket source."),t===void 0)return{ticket:(await T(()=>c.createIssue({team:e.team,project:e.project,title:n,description:r}))).identifier,created:!0};const h=await T(()=>c.issue(t));return h||i(`Linear has no issue ${t}.`),h.team!==e.team&&i(`${t} is in Linear's ${h.team} team, not ${e.team}.`),{ticket:h.identifier,created:!1}}O(u);const s=t===void 0?null:y(t);t!==void 0&&!s&&i("GitHub ticket must be an issue number such as #41.");const p=s?["issue","view",String(s),"--repo",u,"--json","number,url"]:["issue","create","--repo",u,"--title",n,"--body",r],m=await(await j(e,a))("gh",p);m.status!==0&&i((m.stderr||"GitHub refused the issue.").trim().split(`
1
+ var L=Object.defineProperty;var o=(e,t)=>L(e,"name",{value:t,configurable:!0});import{runRepo as $,validateGithub as O}from"../hub/repos.mjs";import{savedClient as k}from"../hub/linear.mjs";const i=o(e=>{throw Object.assign(new Error(e),{status:400})},"refuse");function N(e){(!e||typeof e!="object"||Array.isArray(e))&&i("Tickets must name a source.");const t={internal:["source","prefix"],github:["source","account"],linear:["source","workspace","team","project"]};if(Object.hasOwn(t,e.source)||i("Ticket source must be internal, github or linear."),Object.keys(e).some(n=>!t[e.source].includes(n))&&i("Unknown ticket source field."),e.source==="internal"){const n=e.prefix===void 0||e.prefix===""?"INT":e.prefix;return(typeof n!="string"||!/^[A-Z]{2,6}$/.test(n))&&i("Prefix must be 2\u20136 uppercase letters."),{source:"internal",prefix:n}}return e.source==="github"?e.account===void 0||e.account===""?{source:"github"}:((typeof e.account!="string"||!/^[A-Za-z0-9](?:[A-Za-z0-9-]{0,38})$/.test(e.account))&&i("The gh account must be a GitHub login."),{source:"github",account:e.account}):((typeof e.workspace!="string"||!/^[a-z\d][a-z\d-]*$/i.test(e.workspace))&&i("Linear needs a workspace."),(typeof e.team!="string"||!/^[A-Z][A-Z0-9]*$/.test(e.team))&&i("Linear needs an uppercase team key."),(typeof e.project!="string"||!e.project.trim())&&i("Linear needs a project."),{source:"linear",workspace:e.workspace,team:e.team,project:e.project.trim()})}o(N,"ticketSource");const w=o(e=>{const t=e?Number(e[1]):null;return Number.isSafeInteger(t)?t:null},"numberOf"),y=o(e=>w(/^#?([1-9]\d*)$/.exec(String(e??""))),"issueNumber"),x=o(e=>e.source==="internal"?e.prefix||"INT":e.source==="linear"?e.team:null,"keyOf");function g(e,t){if(!e)return null;const n=x(e),r=String(t??"");return n?r.startsWith(`${n}-`)?w(/^([1-9]\d*)$/.exec(r.slice(n.length+1))):null:w(/^#([1-9]\d*)$/.exec(r))}o(g,"ticketIn");function C(e,t,{repo:n,key:r}={}){const u=g(e,t);if(!u)return null;if(e.source==="internal")return{id:`${x(e)}-${u}`,url:`#selectedCard=${encodeURIComponent(r)}`,source:"internal"};if(e.source==="github")return n?{id:`#${u}`,url:`https://github.com/${n}/issues/${u}`,source:"github"}:null;const l=`${e.team}-${u}`;return{id:l,url:`https://linear.app/${e.workspace}/issue/${l}`,source:"linear"}}o(C,"ticketBadge");async function j(e,t){if(!e.account)return t;const n=await t("gh",["auth","token","-u",e.account]);(n.status!==0||!n.stdout?.trim())&&i(`gh has no ${e.account} account; run gh auth login`);const r={...process.env,GH_TOKEN:n.stdout.trim()};return(u,l)=>t(u,l,{env:r})}o(j,"issueRunner");const T=o(async e=>{try{return await e()}catch(t){i(t.message)}},"linearCall");async function E(e,{ticket:t,title:n,body:r="",repo:u,next:l},{run:a=$,linearFetch:b}={}){if(e.source==="internal")return t!==void 0&&!g(e,t)&&i(`Ticket must match ${e.prefix}-<number>.`),{ticket:t??`${e.prefix}-${l}`,created:t===void 0};if(e.source==="linear"){t!==void 0&&!g(e,t)&&i(`Ticket must match ${e.team}-<number>.`);const c=k({fetch:b});if(c||i("Hub has no Linear key, so this board can\u2019t create Linear tickets; add it in Hub settings \u203A Linear, or pick another ticket source."),t===void 0)return{ticket:(await T(()=>c.createIssue({team:e.team,project:e.project,title:n,description:r}))).identifier,created:!0};const h=await T(()=>c.issue(t));return h||i(`Linear has no issue ${t}.`),h.team!==e.team&&i(`${t} is in Linear's ${h.team} team, not ${e.team}.`),{ticket:h.identifier,created:!1}}O(u);const s=t===void 0?null:y(t);t!==void 0&&!s&&i("GitHub ticket must be an issue number such as #41.");const p=s?["issue","view",String(s),"--repo",u,"--json","number,url"]:["issue","create","--repo",u,"--title",n,"--body",r],m=await(await j(e,a))("gh",p);m.status!==0&&i((m.stderr||"GitHub refused the issue.").trim().split(`
2
2
  `)[0]);let f;if(s){let c;try{c=JSON.parse(m.stdout)}catch{i("GitHub returned an invalid issue.")}c.number!==s&&i("GitHub returned a different issue."),f=c.url}else f=m.stdout?.trim();const d=/^https:\/\/github\.com\/([^/]+\/[^/]+)\/issues\/([1-9]\d*)$/.exec(f||"");return(!d||d[1].toLowerCase()!==u.toLowerCase()||s&&Number(d[2])!==s||!y(d[2]))&&i("Ticket must be an issue in the board\u2019s repo."),{ticket:`#${d[2]}`,created:!s}}o(E,"resolveTicket");const Z=o(e=>e.startsWith("#")?`GH-${e.slice(1)}`:e,"cardIdOf");function R(e,t){const n=e.source==="github"?String(t??"").match(/(?<![\w#])#[1-9]\d*\b/g):String(t??"").match(/\b[A-Z][A-Z0-9]*-[1-9]\d*\b/g);return[...new Set((n||[]).filter(r=>g(e,r)))]}o(R,"titleTickets");function z({start:e,done:t,handover:n}){return t?"done":e?"started":n?"review":null}o(z,"ticketTarget");const S={started:"In Progress",review:"In Review",done:"Done"};async function _(e,t,n,{repo:r,run:u=$,linearFetch:l}={}){const a=t.ticket;if(!e||!n||!a||!g(e,a)||e.source==="internal"||e.source==="github"&&n!=="done")return null;if(e.source==="github"&&!r)return`ticket ${a}: not closed, the board has no GitHub repo`;const b=o(s=>`ticket ${a}: could not move to ${S[n]} (${String(s.message||s).split(`
3
3
  `)[0]})`,"why");try{if(e.source==="linear"){const f=k({fetch:l});if(!f)return`ticket ${a}: not moved, Hub has no Linear key`;if(!t.ticket_created){const c=await f.issue(a);if(!c)return`ticket ${a}: not moved, Linear has no such issue`;if(c.assignee!==f.saved.viewer?.id)return`ticket ${a}: left as it is, not assigned to you`}const d=await f.moveIssue(a,n);return d?`ticket ${a}: moved to ${d}`:null}const s=a.slice(1),p=await j(e,u);if(!t.ticket_created){const f=await p("gh",["issue","view",s,"--repo",r,"--json","state,assignees"]);if(f.status!==0)throw new Error(f.stderr||"gh issue view failed");const d=await p("gh",["api","user","--jq",".login"]);if(d.status!==0)throw new Error(d.stderr||"gh api user failed");const c=JSON.parse(f.stdout);if(c.state!=="OPEN")return null;if(!(c.assignees||[]).some(h=>h.login?.toLowerCase()===d.stdout.trim().toLowerCase()))return`ticket ${a}: left as it is, not assigned to you`}const m=await p("gh",["issue","close",s,"--repo",r]);if(m.status!==0)throw new Error(m.stderr||"gh issue close failed");return`ticket ${a}: closed`}catch(s){return b(s)}}o(_,"moveTicket");export{Z as cardIdOf,_ as moveTicket,E as resolveTicket,C as ticketBadge,g as ticketIn,N as ticketSource,z as ticketTarget,R as titleTickets};
@@ -1,2 +1,4 @@
1
- var m=Object.defineProperty;var s=(r,e)=>m(r,"name",{value:e,configurable:!0});import c from"node:fs";import l from"node:os";import a from"node:path";const d=256*1024;function p(r,e){const t=Math.min(e,d),n=Buffer.alloc(t),o=c.openSync(r,"r");try{c.readSync(o,n,0,t,e-t)}finally{c.closeSync(o)}for(const u of n.toString("utf8").split(`
2
- `).reverse()){let i;try{i=JSON.parse(u).message?.content}catch{continue}if(Array.isArray(i))return i.at(-1)?.type==="tool_use"}return!1}s(p,"endsOnToolCall");function f(r,e){const t=a.join(e,".claude","projects",r.replace(/[/.]/g,"-"));return c.readdirSync(t).filter(n=>n.endsWith(".jsonl")).map(n=>({file:a.join(t,n),stat:c.statSync(a.join(t,n))})).reduce((n,o)=>n&&n.stat.mtimeMs>=o.stat.mtimeMs?n:o,void 0)}s(f,"newestTranscript");function j(r,e=l.homedir()){try{return f(r,e)?.stat.mtimeMs??null}catch{return null}}s(j,"transcriptMovedAt");function M(r,e=l.homedir()){try{const t=f(r,e);return t!==void 0&&p(t.file,t.stat.size)}catch{return!1}}s(M,"transcriptCommandRunning");export{M as transcriptCommandRunning,j as transcriptMovedAt};
1
+ var h=Object.defineProperty;var s=(t,n)=>h(t,"name",{value:n,configurable:!0});import a from"node:fs";import d from"node:os";import u from"node:path";const b=256*1024;function x(t,n){const e=Math.min(n,b),o=Buffer.alloc(e),r=a.openSync(t,"r");try{a.readSync(r,o,0,e,n-e)}finally{a.closeSync(r)}for(const i of o.toString("utf8").split(`
2
+ `).reverse()){let l;try{l=JSON.parse(i).message?.content}catch{continue}if(Array.isArray(l))return l.at(-1)?.type==="tool_use"}return!1}s(x,"endsOnToolCall");function y(t,n){let e=t;try{e=a.realpathSync(t)}catch{}const o=u.join(n,".claude","projects",e.replace(/[/.]/g,"-"));return a.readdirSync(o).filter(r=>r.endsWith(".jsonl")).map(r=>({file:u.join(o,r),stat:a.statSync(u.join(o,r))})).reduce((r,i)=>r&&r.stat.mtimeMs>=i.stat.mtimeMs?r:i,void 0)}s(y,"newestTranscript");function A(t,n=d.homedir()){try{return y(t,n)?.stat.mtimeMs??null}catch{return null}}s(A,"transcriptMovedAt");function C(t,n=d.homedir()){try{const e=y(t,n);return e!==void 0&&x(e.file,e.stat.size)}catch{return!1}}s(C,"transcriptCommandRunning");const p=4e3,m=s(t=>t.length>p?`${t.slice(0,p)}\u2026`:t,"cut"),g=s(t=>typeof t=="string"?t:t.map(n=>n.text??"").join(`
3
+ `),"textOf"),S=s(t=>t.command??t.file_path??t.pattern??t.url??t.description??JSON.stringify(t),"callOf"),O=s(t=>{try{const n=JSON.parse(t);return typeof n?.type=="string"?n:null}catch{return null}},"eventOf");function w(t){if(t.type==="system")return t.subtype==="init"?[{kind:"sys",label:"start",body:`${t.model} in ${t.cwd}`}]:[];if(t.type==="result")return[{kind:"text end",label:t.is_error?"stopped":"done",body:t.result??t.subtype}];if(t.type!=="assistant"&&t.type!=="user")return[];const n=t.message.content;return typeof n=="string"?[{kind:"text",body:n}]:n.flatMap(e=>e.type==="text"?[{kind:"text",body:e.text}]:e.type==="tool_use"?[{kind:"tool",id:e.id,label:e.name.toLowerCase(),body:S(e.input)}]:e.type==="tool_result"?[{kind:"out",for:e.tool_use_id,body:m(g(e.content))}]:[])}s(w,"rowsOf");function _(t,n){const e=t.lastIndexOf(10)+1,o=t.subarray(0,e).toString("utf8").split(`
4
+ `).slice(0,-1),r=[];let i=!1,l=!1;for(const f of o){const c=O(f);if(!c){r.push({kind:"out",body:m(f)});continue}i=!0,l||=c.type==="result",r.push(...w(c))}return{rows:r,next:n+e,stream:i,finished:l}}s(_,"transcriptChunk");async function R(t,n){const e=await a.promises.open(t,"r");try{const{size:o}=await e.stat(),r=Buffer.alloc(Math.max(0,o-n)),{bytesRead:i}=await e.read(r,0,r.length,n);return{..._(r.subarray(0,i),n),size:o}}finally{await e.close()}}s(R,"readRunTranscript");export{R as readRunTranscript,_ as transcriptChunk,C as transcriptCommandRunning,A as transcriptMovedAt};
@@ -1,9 +1,10 @@
1
1
  #!/usr/bin/env node
2
- // A PreToolUse hook on Edit|Write that refuses a hand edit of the JSON files board.mjs owns on a board site: the
3
- // site's board.json, every .json under its data/, and the wave file(s) board.json's `items` names. It denies, not asks,
4
- // because a headless session has nobody to ask, and the reason names the board.mjs command that makes the change.
5
- // Notes .md files, question drafts (<site>/drafts/) and Bash writes stay open
6
- // (docs/adr/2026-11-05-2330-a-hook-refuses-hand-edits-to-board-data-files.md).
2
+ // A PreToolUse hook on Edit|Write that refuses a hand edit of the files board.mjs owns on a board site: the site's
3
+ // board.json, every .json and .md under its data/, the wave file(s) board.json's `items` names, and the .md notes beside
4
+ // a ~/.claude/waves wave file. It denies, not asks, because a headless session has nobody to ask, and the reason names
5
+ // the board.mjs command that makes the change. Drafts (<site>/drafts/, or any folder outside data/) and Bash writes stay
6
+ // open (docs/adr/2026-11-05-2330-a-hook-refuses-hand-edits-to-board-data-files.md,
7
+ // docs/adr/2026-11-09-1100-the-board-owns-a-cards-notes-file.md).
7
8
  import fs from 'node:fs';
8
9
  import os from 'node:os';
9
10
  import path from 'node:path';
@@ -14,13 +15,14 @@ process.stdout.on('error', () => {});
14
15
  const real = (p) => { try { return fs.realpathSync(p); } catch { return path.resolve(p); } };
15
16
  const inside = (dir, file) => !path.relative(dir, file).startsWith('..') && !path.isAbsolute(path.relative(dir, file));
16
17
 
17
- // → the file's kind on this board ('wave', 'board' or 'data'), or null when it is not board-owned
18
+ // → the file's kind on this board ('wave', 'board', 'notes' or 'data'), or null when it is not board-owned
18
19
  function ownedKind(board, file) {
19
20
  const dir = real(board.dir);
20
21
  let items = 'waves';
21
22
  try { items = JSON.parse(fs.readFileSync(path.join(dir, 'board.json'), 'utf8')).items || 'waves'; } catch {}
22
23
  if (file === path.join(dir, 'board.json')) return 'board';
23
24
  if (items === 'waves' ? path.basename(file) === 'wave.json' && inside(real(path.join(os.homedir(), '.claude', 'waves')), file) : file === real(path.resolve(dir, items))) return 'wave';
25
+ if (file.endsWith('.md') && (inside(path.join(dir, 'data'), file) || (items === 'waves' && inside(real(path.join(os.homedir(), '.claude', 'waves')), file)))) return 'notes';
24
26
  if (file.endsWith('.json') && inside(path.join(dir, 'data'), file)) return 'data';
25
27
  return null;
26
28
  }
@@ -28,6 +30,7 @@ function ownedKind(board, file) {
28
30
  const HINTS = {
29
31
  wave: 'A card changes with `board.mjs --site <route> item add|set <wave>/<id> …`',
30
32
  board: 'Board settings change with `board.mjs --site <route> rules|reviewers …` or the settings page',
33
+ notes: 'A card\'s notes change with `board.mjs --site <route> item notes <wave>/<id> --heading "<heading>" --file <draft.md>`, the draft written outside data/',
31
34
  data: 'Board data changes with a board.mjs command (`item set`, `thread`, `topic add`, `question add --file <draft>`, `reply`, `builder …`); its usage text lists them',
32
35
  };
33
36
 
@@ -1,4 +1,4 @@
1
- var u=Object.defineProperty;var a=(t,e)=>u(t,"name",{value:e,configurable:!0});import d from"node:fs";import g from"node:net";import c from"node:path";import{fileURLToPath as y}from"node:url";const f=c.join(c.dirname(y(import.meta.url)),".."),p={"view-only":{status:403,line:"You can look at this board, not change it.",hint:"Its owner shared it with you view only.",json:{error:"This board is shared with you to look at only."}},other:{status:403,line:"This kickflip belongs to someone else.",hint:"Ask its owner to share it with you.",json:{}},unread:{status:503,line:"Tailscale didn\u2019t answer, so this kickflip can\u2019t tell who you are.",hint:"Open it on the Mac itself, or try again in a moment.",json:{error:"Tailscale didn\u2019t answer, so the hub can\u2019t tell who you are."}}},b=`html, body { margin: 0; height: 100%; }
1
+ var u=Object.defineProperty;var a=(t,e)=>u(t,"name",{value:e,configurable:!0});import d from"node:fs";import g from"node:net";import c from"node:path";import{fileURLToPath as y}from"node:url";const f=c.join(c.dirname(y(import.meta.url)),".."),p={"view-only":{status:403,line:"You can look at this board, not change it.",hint:"Its owner shared it with you view only.",json:{error:"This board is shared with you to look at only."}},other:{status:403,line:"This kickflip belongs to someone else.",hint:"Ask its owner to share it with you.",json:{}},unread:{status:503,line:"Tailscale didn\u2019t answer, so this kickflip can\u2019t tell who you are.",hint:"Open it on the host itself, or try again in a moment.",json:{error:"Tailscale didn\u2019t answer, so the hub can\u2019t tell who you are."}}},b=`html, body { margin: 0; height: 100%; }
2
2
  body { display: grid; place-content: center; justify-items: center; gap: 10px; padding: 32px 24px; box-sizing: border-box; background: var(--bg); color: var(--text); text-align: center;
3
3
  font: 14px/1.45 -apple-system, BlinkMacSystemFont, "SF Pro Text", "Segoe UI", system-ui, sans-serif; }
4
4
  .rf-mark { display: block; margin-bottom: 8px; width: 70px; height: 60px; object-fit: contain; }
package/hub/home.mjs CHANGED
@@ -1 +1 @@
1
- var g=Object.defineProperty;var a=(t,e)=>g(t,"name",{value:e,configurable:!0});import{siteIcon as f}from"./icons.mjs";import{contextFull as w}from"./usage.mjs";const d=a(t=>Number.isInteger(t)&&t>0?t:0,"count");function A(t){const{items:e,working:i,summary:r,lanes:n,building:o}=t||{};if(!Array.isArray(e))return null;const s=[...new Set(e.map(l=>String(l?.kind||"").trim()).filter(Boolean))],u=n&&typeof n=="object"&&["review","progress","todo"].every(l=>n[l]===void 0||Number.isInteger(n[l])&&n[l]>=0);return{count:e.length,working:d(i),summary:typeof r=="string"&&r.trim()?r.trim():e.length?`${e.length} waiting${s.length?`: ${s.join(", ")}`:""}`:"",lanes:u?{review:d(n.review),progress:d(n.progress),todo:d(n.todo)}:null,building:o===!0}}a(A,"readNeeds");function h({state:t,working:e,count:i,building:r,listen:n}={}){return t==="down"?{tone:"down",word:"down"}:n==="full"?{tone:"down",word:"context full"}:n==="deaf"?{tone:"down",word:"not listening"}:t==="starting"?{tone:"starting",word:"starting"}:t==="stopped"?{tone:"stopped",word:"stopped"}:i?{tone:"needs",word:`${i} need you`}:e?{tone:"working",word:"working"}:n==="waking"?{tone:"asleep",word:"waking\u2026"}:n==="asleep"?{tone:"asleep",word:"asleep"}:r?{tone:"building",word:"implementing"}:null}a(h,"siteStatus");function $(t){return t?[["review","review"],["progress","doing"],["todo","todo"]].filter(([e])=>t[e]>0).map(([e,i])=>({lane:e,text:`${t[e]} ${i}`})):null}a($,"laneChips");const c=a(t=>String(t??"").replace(/[&<>"']/g,e=>({"&":"&amp;","<":"&lt;",">":"&gt;",'"':"&quot;","'":"&#39;"})[e]),"esc");function m(t,e,i){const r=h({state:t,...e,listen:i}),n=t==="up"?$(e?.lanes):null,o=r?`<span class="dot t-${r.tone}${r.tone==="working"?" pulse":""}" aria-hidden="true"></span><span class="word t-${r.tone}">${c(r.word)}</span>`:"",s=n===null?"":n.length?n.map(u=>`<span class="lc lc-${u.lane}">${c(u.text)}</span>`).join(""):"<span>nothing open</span>";return o||s?`${o}${s?`<span class="lcs">${s}</span>`:""}`:""}a(m,"statusLine");function v(t,e,i,r=new Map,n=new Map){return t.map(o=>{const s=e.get(o.route)??null,u=i.has(o.route)?r.get(o.route)??"listening":void 0,l=n.get(o.route)??null;return{route:o.route,title:o.title,board:i.has(o.route),state:o.state,count:s?.count||0,changedAt:o.changedAt||0,created:o.created||null,icon:f(o.route,o),sub:m(o.state,s,w(u,l)?"full":u),ctx:l}})}a(v,"sidebarSites");function S(t){return t.filter(e=>e.board).sort((e,i)=>i.count-e.count||i.changedAt-e.changedAt||e.route.localeCompare(i.route))[0]?.route??null}a(S,"startRoute");const x="runs older code than its files; restart it";function j(t,e=new Map,i=null){const r=t.filter(u=>u.state==="down").length,n=t.reduce((u,l)=>u+(e.get(l.route)||[]).filter(p=>p.state==="broken").length,0),o=t.filter(u=>u.state==="up").length,s=i?.ok===!1;return{tone:r?"down":n||s?"wait":"ok",word:r?`${r} site${r===1?"":"s"} down`:n||s?"degraded":"healthy",detail:`${o} of ${t.length} site${t.length===1?"":"s"} up${n?` \xB7 ${n} link${n===1?"":"s"} failing`:""}${s?` \xB7 ${x}`:""}`}}a(j,"hubHealth");export{x as STALE_CODE,j as hubHealth,$ as laneChips,A as readNeeds,v as sidebarSites,h as siteStatus,S as startRoute,m as statusLine};
1
+ var g=Object.defineProperty;var a=(e,t)=>g(e,"name",{value:t,configurable:!0});import{siteIcon as w}from"./icons.mjs";import{contextFull as f}from"./usage.mjs";const d=a(e=>Number.isInteger(e)&&e>0?e:0,"count");function A(e){const{items:t,working:i,summary:n,lanes:r,building:o}=e||{};if(!Array.isArray(t))return null;const l=[...new Set(t.map(u=>String(u?.kind||"").trim()).filter(Boolean))],s=r&&typeof r=="object"&&["review","progress","todo"].every(u=>r[u]===void 0||Number.isInteger(r[u])&&r[u]>=0);return{count:t.length,working:d(i),summary:typeof n=="string"&&n.trim()?n.trim():t.length?`${t.length} waiting${l.length?`: ${l.join(", ")}`:""}`:"",lanes:s?{review:d(r.review),progress:d(r.progress),todo:d(r.todo)}:null,building:o===!0}}a(A,"readNeeds");function h({state:e,working:t,building:i,listen:n}={}){return e==="down"?{tone:"down",word:"down"}:n==="full"?{tone:"down",word:"context full"}:n==="deaf"?{tone:"down",word:"not listening"}:e==="starting"?{tone:"starting",word:"starting"}:e==="stopped"?{tone:"stopped",word:"stopped"}:t?{tone:"working",word:"working"}:n==="waking"?{tone:"asleep",word:"waking\u2026"}:n==="asleep"?{tone:"asleep",word:"asleep"}:i?{tone:"building",word:"implementing"}:null}a(h,"siteStatus");function $(e){return e?[["review","review"],["progress","doing"],["todo","todo"]].filter(([t])=>e[t]>0).map(([t,i])=>({lane:t,text:`${e[t]} ${i}`})):null}a($,"laneChips");const c=a(e=>String(e??"").replace(/[&<>"']/g,t=>({"&":"&amp;","<":"&lt;",">":"&gt;",'"':"&quot;","'":"&#39;"})[t]),"esc");function m(e,t,i){const n=h({state:e,...t,listen:i}),r=e==="up"?$(t?.lanes):null,o=t?.count?[{lane:"needs",text:`${t.count} need you`},...r||[]]:r,l=n?`<span class="dot t-${n.tone}${n.tone==="working"?" pulse":""}" aria-hidden="true"></span><span class="word t-${n.tone}">${c(n.word)}</span>`:"",s=o===null?"":o.length?o.map(u=>`<span class="lc lc-${u.lane}">${c(u.text)}</span>`).join(""):"<span>nothing open</span>";return l||s?`${l}${s?`<span class="lcs">${s}</span>`:""}`:""}a(m,"statusLine");function v(e,t,i,n=new Map,r=new Map){return e.map(o=>{const l=t.get(o.route)??null,s=i.has(o.route)?n.get(o.route)??"listening":void 0,u=r.get(o.route)??null;return{route:o.route,title:o.title,board:i.has(o.route),state:o.state,count:l?.count||0,changedAt:o.changedAt||0,created:o.created||null,icon:w(o.route,o),sub:m(o.state,l,f(s,u)?"full":s),ctx:u}})}a(v,"sidebarSites");function S(e){return e.filter(t=>t.board).sort((t,i)=>i.count-t.count||i.changedAt-t.changedAt||t.route.localeCompare(i.route))[0]?.route??null}a(S,"startRoute");const x="runs older code than its files; restart it";function j(e,t=new Map,i=null){const n=e.filter(s=>s.state==="down").length,r=e.reduce((s,u)=>s+(t.get(u.route)||[]).filter(p=>p.state==="broken").length,0),o=e.filter(s=>s.state==="up").length,l=i?.ok===!1;return{tone:n?"down":r||l?"wait":"ok",word:n?`${n} site${n===1?"":"s"} down`:r||l?"degraded":"healthy",detail:`${o} of ${e.length} site${e.length===1?"":"s"} up${r?` \xB7 ${r} link${r===1?"":"s"} failing`:""}${l?` \xB7 ${x}`:""}`}}a(j,"hubHealth");export{x as STALE_CODE,j as hubHealth,$ as laneChips,A as readNeeds,v as sidebarSites,h as siteStatus,S as startRoute,m as statusLine};
package/hub/hub.mjs CHANGED
@@ -1,16 +1,16 @@
1
1
  #!/usr/bin/env node
2
- var Ft=Object.defineProperty;var l=(e,t)=>Ft(e,"name",{value:t,configurable:!0});import Ue from"node:http";import Nt from"node:net";import{spawn as Be,execFileSync as Gt}from"node:child_process";import b from"node:fs";import $ from"node:path";import Fe from"node:os";import{fileURLToPath as zt}from"node:url";import Ne from"node:vm";import Wt from"node:zlib";import{routeRecord as R}from"./routes.mjs";import{publicKey as Jt,addSubscription as Kt,removeSubscription as Vt,removeSubscriptionById as Zt,sendAll as Qt,flushQueue as Ge,queueStatus as ze,saveSettings as Xt,readSettings as ee,migrateSettings as Yt,setNotifications as qt,readCodexShare as We,setCodexShare as en,readClaudeModel as tn,setClaudeModel as nn,deliver as sn}from"./push.mjs";import{ownerTracker as an}from"./presence.mjs";import{POLL_MS as Je,answerOf as rn,expired as on,pendingFor as Ke,promptRecord as cn,pushOf as ln,updatePrompts as G,shown as un,pruneRemembered as ye,rememberRules as dn,rememberedAnswer as hn,rememberedFor as Ve}from"./prompts.mjs";import{hold as fn,heldUntil as pn,holdLine as mn,editQuiet as gn,checkEvery as Ze,nextCheckAt as wn,LOW_POINTS as bn}from"./schedule.mjs";import{capital as yn,hubView as Sn,settingsView as kn,flushView as $n,SNOOZE_REFUSED as _n,AWAKE_REFUSED as xn}from"./views.mjs";import{caffeinate as Tn,setKeepAwake as vn,awakeUntil as Qe,awakeUnsupported as On}from"./awake.mjs";import{endOf as jn}from"./duration.mjs";import{checkGitHub as Pn,sendWakes as Rn}from"./github.mjs";import{checkIncoming as En}from"./incoming.mjs";import{listBoards as D,isListening as Dn,isBoardSite as Se,listenState as Xe,RESUME_STATE as Ln,sendToBoards as Ye,checkListening as Cn,recordHubCode as An,hubCode as In,runningHubCode as Mn,boardCode as qe,recordLinkFaults as Hn,linksOf as Un,hubLink as Bn,hubCodeState as Fn,readJSON as et}from"./boards.mjs";import{liveSessions as tt}from"./live.mjs";import{RuleError as Nn,applyRules as Gn,changed as nt,readHubRules as ke,writeHubRules as zn}from"./rules.mjs";import{siteIcon as Wn}from"./icons.mjs";import{readNeeds as Jn,sidebarSites as Kn,startRoute as Vn,hubHealth as Zn}from"./home.mjs";import{checkSessions as Qn,compactFullContexts as Xn,startingBoards as Yn}from"./sessions.mjs";import{repoService as qn,repoLookupRoute as es}from"./repos.mjs";import{boardRepo as st,changeBoardRepo as ts,migrateBoardRepos as ns,migrateHubBoard as ss}from"./board-repo.mjs";import{requestBoard as as,retryBoard as rs,dismissBoard as os,clonePercent as is,settleStarts as cs}from"./new-board.mjs";import{boardClosure as ls,closeBoard as us,closedBoards as ds,reopenBoard as hs}from"./close-board.mjs";import{restartBoard as fs,restartable as ps}from"./restart-board.mjs";import{crossSite as ms,isJSON as $e}from"../starter/cross-site.mjs";import{BOARD_PUBLIC as _e,appFile as gs,colorsVersion as at,destOf as ws,fileVersion as bs,pageOf as rt,shellHtml as ys}from"./shell.mjs";import{hubStream as Ss}from"./stream.mjs";import{usageReader as ks,checkUsagePause as $s,contextReading as _s}from"./usage.mjs";import{readUsageGate as te,setPauseAt as xs,setKeepGoing as Ts,setHubPause as vs,boardPausedUntil as Os}from"../shared/usage-gate.mjs";import{checkMachine as js}from"../shared/machine-gate.mjs";import{readiness as Ps,readCodex as ne}from"./readiness.mjs";import{BuildError as Rs,startBuild as Es,stopTries as Ds,endUnsharedTries as Ls,tryTick as xe,initializeTries as Cs,fetchTryBranches as ot,pullNow as As,pullMains as Is}from"./try.mjs";import{serveFiles as Ms}from"./files.mjs";import{upgrader as Hs,CHECK_MS as Us}from"./upgrade.mjs";import{restarter as Bs}from"./self-restart.mjs";import{fallBackFromLinear as Fs,openBoardBuilders as Ns}from"../board/store.mjs";import{recordRefusal as Gs}from"../board/permissions.mjs";import{hubGate as Te}from"../board/on-host.mjs";import{pipeUpgrade as zs,refuse as Ws,refuseUpgrade as Js}from"./front-door.mjs";import{shareEntries as Ks,sharedRoutes as it,shareReach as Vs,routeOfPath as Zs,sharedSidebar as Qs,shareTracker as Xs}from"./share.mjs";import{restartRunner as ct}from"../board/worktree.mjs";import{hubService as Ys}from"../shared/service.mjs";import{cloneRoot as ve}from"../shared/repos.mjs";import{LinearError as Oe,linearStatus as se,removeLinearKey as qs,saveLinearKey as ea,savedClient as lt}from"./linear.mjs";import{SlackError as ta,slackStatus as na,removeSlackToken as sa,saveSlackToken as aa}from"./slack.mjs";import{sendReviews as ra}from"./review-send.mjs";const z=$.dirname(zt(import.meta.url)),oa=$.join(z,"..","shared","colors.css"),ut=JSON.parse(b.readFileSync($.join(z,"..","package.json"),"utf8")).version,c=l((...e)=>console.log(new Date().toISOString(),...e),"log"),W=$.join(Fe.homedir(),".claude","kickflip"),dt=$.join(W,"manifest.json"),ae=$.join(W,"logs"),ht=$.join(W,"run"),je=Number(process.env.KICKFLIP_HUB_PORT||4500);for(const e of[W,ae,ht])b.mkdirSync(e,{recursive:!0});try{const e=An();c(`hub code ${e.hash} (${e.files.length} files)`)}catch(e){c("could not record the hub code hash:",e.message)}try{const e=await ns();e.length&&c(`manifest: recorded GitHub repos for ${e.map(t=>`/${t}`).join(", ")}`)}catch(e){c("board repo migration failed:",e.message)}try{const e=await ss();e&&c(`manifest: /${e}, the hub board, builds from its own folder's local main`)}catch(e){c("hub board migration failed:",e.message)}const M=qn(),ia=ks(),re=Tn({log:c});re.apply();let oe={version:1,sites:{}};function g(){try{oe=JSON.parse(b.readFileSync(dt,"utf8")),oe.sites||={}}catch(e){e.code==="ENOENT"?oe={version:1,sites:{}}:c("manifest unreadable, keeping the last good copy:",e.message)}return oe}l(g,"loadManifest");const j=new Map,H=new Set;let ie=!1;const ft=5e3,ce=l(e=>JSON.stringify([e.cmd,e.cwd||e.dir,e.port,e.env||{},e.restart_at||0]),"keyOf"),Pe=l(e=>Array.isArray(e?.cmd)&&e.cmd.length>0&&!!e.port,"runs"),U=l(e=>Pe(e)?"cmd":e?.port?"port":"files","servesOf"),J=new Map;function le(e){return $.join(ht,e+".pid")}l(le,"pidFile");function ca(e,t){try{const n=Number(b.readFileSync(le(e),"utf8")),s=n?Gt("ps",["-o","command=","-p",String(n)],{encoding:"utf8"}).trim():"";s&&s.includes(t.cmd[t.cmd.length-1])&&(process.kill(-n,"SIGTERM"),c(`stopped stale ${e} (pgid ${n})`))}catch{}try{b.unlinkSync(le(e))}catch{}}l(ca,"killStale");function la(e,t){const n=j.get(e)||{restarts:0},s=b.openSync($.join(ae,e+".log"),"a"),[i,...a]=t.cmd,p={...process.env,...t.env||{},PORT:String(t.port),PREFIX:"/"+e,KICKFLIP_ROUTE:e};b.writeSync(s,`
2
+ var Nt=Object.defineProperty;var l=(e,t)=>Nt(e,"name",{value:t,configurable:!0});import Ue from"node:http";import Gt from"node:net";import{spawn as Be,execFileSync as zt}from"node:child_process";import b from"node:fs";import $ from"node:path";import Fe from"node:os";import{fileURLToPath as Kt}from"node:url";import Ne from"node:vm";import Wt from"node:zlib";import{routeRecord as R}from"./routes.mjs";import{publicKey as Jt,addSubscription as Vt,removeSubscription as Zt,removeSubscriptionById as Qt,sendAll as Xt,flushQueue as Ge,queueStatus as ze,saveSettings as Yt,readSettings as ee,migrateSettings as qt,setNotifications as en,readCodexShare as Ke,setCodexShare as tn,readClaudeModel as nn,setClaudeModel as sn,deliver as an}from"./push.mjs";import{ownerTracker as rn}from"./presence.mjs";import{POLL_MS as We,answerOf as on,expired as cn,pendingFor as Je,promptRecord as ln,pushOf as un,updatePrompts as G,shown as dn,pruneRemembered as ye,rememberRules as hn,rememberedAnswer as fn,rememberedFor as Ve}from"./prompts.mjs";import{hold as pn,heldUntil as mn,holdLine as gn,editQuiet as wn,checkEvery as Ze,nextCheckAt as bn,LOW_POINTS as yn}from"./schedule.mjs";import{capital as Sn,hubView as kn,settingsView as $n,flushView as _n,SNOOZE_REFUSED as xn,AWAKE_REFUSED as Tn}from"./views.mjs";import{caffeinate as vn,setKeepAwake as On,awakeUntil as Qe,awakeUnsupported as jn}from"./awake.mjs";import{endOf as Pn}from"./duration.mjs";import{checkGitHub as Rn,sendWakes as En}from"./github.mjs";import{checkIncoming as Dn}from"./incoming.mjs";import{listBoards as D,isListening as Ln,isBoardSite as Se,listenState as Xe,RESUME_STATE as Cn,sendToBoards as Ye,checkListening as An,recordHubCode as In,hubCode as Mn,runningHubCode as Hn,boardCode as qe,recordLinkFaults as Un,linksOf as Bn,hubLink as Fn,hubCodeState as Nn,readJSON as et}from"./boards.mjs";import{liveSessions as tt}from"./live.mjs";import{RuleError as Gn,applyRules as zn,changed as nt,readHubRules as ke,writeHubRules as Kn}from"./rules.mjs";import{siteIcon as Wn}from"./icons.mjs";import{readNeeds as Jn,sidebarSites as Vn,startRoute as Zn,hubHealth as Qn}from"./home.mjs";import{checkSessions as Xn,compactFullContexts as Yn,startingBoards as qn}from"./sessions.mjs";import{repoService as es,repoLookupRoute as ts}from"./repos.mjs";import{boardRepo as st,changeBoardRepo as ns,migrateBoardRepos as ss,migrateHubBoard as as}from"./board-repo.mjs";import{requestBoard as rs,retryBoard as os,dismissBoard as is,clonePercent as cs,settleStarts as ls}from"./new-board.mjs";import{boardClosure as us,closeBoard as ds,closedBoards as hs,reopenBoard as fs}from"./close-board.mjs";import{restartBoard as ps,restartable as ms}from"./restart-board.mjs";import{crossSite as gs,isJSON as $e}from"../starter/cross-site.mjs";import{BOARD_PUBLIC as _e,appFile as ws,colorsVersion as at,destOf as bs,fileVersion as ys,pageOf as rt,shellHtml as Ss}from"./shell.mjs";import{hubStream as ks}from"./stream.mjs";import{usageReader as $s,checkUsagePause as _s,contextReading as xs}from"./usage.mjs";import{readUsageGate as te,setPauseAt as Ts,setKeepGoing as vs,setHubPause as Os,boardPausedUntil as js}from"../shared/usage-gate.mjs";import{checkMachine as Ps}from"../shared/machine-gate.mjs";import{readiness as Rs,readCodex as ne}from"./readiness.mjs";import{BuildError as Es,startBuild as Ds,stopTries as Ls,endUnsharedTries as Cs,tryTick as xe,initializeTries as As,fetchTryBranches as ot,pullNow as Is,pullMains as Ms}from"./try.mjs";import{serveFiles as Hs}from"./files.mjs";import{upgrader as Us,CHECK_MS as Bs}from"./upgrade.mjs";import{restarter as Fs}from"./self-restart.mjs";import{fallBackFromLinear as Ns,openBoardBuilders as Gs}from"../board/store.mjs";import{recordRefusal as zs}from"../board/permissions.mjs";import{hubGate as Te}from"../board/on-host.mjs";import{pipeUpgrade as Ks,refuse as Ws,refuseUpgrade as Js}from"./front-door.mjs";import{shareEntries as Vs,sharedRoutes as it,shareReach as Zs,routeOfPath as Qs,sharedSidebar as Xs,shareTracker as Ys}from"./share.mjs";import{restartRunner as ct}from"../board/worktree.mjs";import{hubService as qs}from"../shared/service.mjs";import{cloneRoot as ve}from"../shared/repos.mjs";import{LinearError as Oe,linearStatus as se,removeLinearKey as ea,saveLinearKey as ta,savedClient as lt}from"./linear.mjs";import{SlackError as na,slackStatus as sa,removeSlackToken as aa,saveSlackToken as ra}from"./slack.mjs";import{sendReviews as oa}from"./review-send.mjs";const z=$.dirname(Kt(import.meta.url)),ia=$.join(z,"..","shared","colors.css"),ut=JSON.parse(b.readFileSync($.join(z,"..","package.json"),"utf8")).version,c=l((...e)=>console.log(new Date().toISOString(),...e),"log"),K=$.join(Fe.homedir(),".claude","kickflip"),dt=$.join(K,"manifest.json"),ae=$.join(K,"logs"),ht=$.join(K,"run"),je=Number(process.env.KICKFLIP_HUB_PORT||4500);for(const e of[K,ae,ht])b.mkdirSync(e,{recursive:!0});try{const e=In();c(`hub code ${e.hash} (${e.files.length} files)`)}catch(e){c("could not record the hub code hash:",e.message)}try{const e=await ss();e.length&&c(`manifest: recorded GitHub repos for ${e.map(t=>`/${t}`).join(", ")}`)}catch(e){c("board repo migration failed:",e.message)}try{const e=await as();e&&c(`manifest: /${e}, the hub board, builds from its own folder's local main`)}catch(e){c("hub board migration failed:",e.message)}const M=es(),ca=$s(),re=vn({log:c});re.apply();let oe={version:1,sites:{}};function g(){try{oe=JSON.parse(b.readFileSync(dt,"utf8")),oe.sites||={}}catch(e){e.code==="ENOENT"?oe={version:1,sites:{}}:c("manifest unreadable, keeping the last good copy:",e.message)}return oe}l(g,"loadManifest");const j=new Map,H=new Set;let ie=!1;const ft=5e3,ce=l(e=>JSON.stringify([e.cmd,e.cwd||e.dir,e.port,e.env||{},e.restart_at||0]),"keyOf"),Pe=l(e=>Array.isArray(e?.cmd)&&e.cmd.length>0&&!!e.port,"runs"),U=l(e=>Pe(e)?"cmd":e?.port?"port":"files","servesOf"),W=new Map;function le(e){return $.join(ht,e+".pid")}l(le,"pidFile");function la(e,t){try{const n=Number(b.readFileSync(le(e),"utf8")),s=n?zt("ps",["-o","command=","-p",String(n)],{encoding:"utf8"}).trim():"";s&&s.includes(t.cmd[t.cmd.length-1])&&(process.kill(-n,"SIGTERM"),c(`stopped stale ${e} (pgid ${n})`))}catch{}try{b.unlinkSync(le(e))}catch{}}l(la,"killStale");function ua(e,t){const n=j.get(e)||{restarts:0},s=b.openSync($.join(ae,e+".log"),"a"),[i,...a]=t.cmd,p={...process.env,...t.env||{},PORT:String(t.port),PREFIX:"/"+e,KICKFLIP_ROUTE:e};b.writeSync(s,`
3
3
  --- ${new Date().toISOString()} hub starting: ${t.cmd.join(" ")} (port ${t.port})
4
4
  `),n.code=Se(t)?qe(t):null;let h;try{h=Be(i,a,{cwd:t.cwd||t.dir,env:p,stdio:["ignore",s,s],detached:!0})}catch(_){b.writeSync(s,`hub could not start it: ${_.message}
5
- `),b.closeSync(s),Object.assign(n,{child:null,key:ce(t),state:"crashed",exitCode:null,nextStart:Date.now()+pt(n.restarts++)}),j.set(e,n);return}b.closeSync(s),b.writeFileSync(le(e),String(h.pid)),Object.assign(n,{child:h,key:ce(t),state:"starting",since:Date.now(),exitCode:null,up:!1}),j.set(e,n),c(`started ${e} pid ${h.pid}`),h.pid&&H.add(h);const u=l((_,k)=>{if(H.delete(h),n.child!==h)return;try{b.unlinkSync(le(e))}catch{}Date.now()-n.since>6e4&&(n.restarts=0),Object.assign(n,{child:null,state:n.stopping?"stopped":"crashed",exitCode:_??k,nextStart:Date.now()+pt(n.restarts++),why:n.stopping?n.why:void 0}),n.stopping=!1,c(`${e} exited (${_??k})`)},"ended");h.on("error",_=>{c(`${e} spawn error: ${_.message}`),h.pid||u(_.code,null)}),h.on("exit",u)}l(la,"start");const pt=l(e=>Math.min(6e4,1e3*2**Math.min(e,6)),"backoff");function ue(e){const t=j.get(e);if(!t||!t.child){j.delete(e);return}t.stopping=!0;const n=t.child;de(n,"SIGTERM"),setTimeout(()=>{H.has(n)&&de(n,"SIGKILL")},ft)}l(ue,"stop");const de=l((e,t)=>{try{process.kill(-e.pid,t)}catch{}},"signal");function Re(e){return new Promise(t=>{const n=Nt.connect({port:e,host:"127.0.0.1"}),s=l(i=>{n.destroy(),t(i)},"done");n.setTimeout(800,()=>s(!1)),n.on("connect",()=>s(!0)),n.on("error",()=>s(!1))})}l(Re,"probe");let mt=!0;const B=new Set;let Ee=!1;const K=new Set,ua=3e4;async function da(e){const t=Date.now()+ua;for(;;){const n=R(g().sites,e),s=j.get(e);if(n&&s?.child&&s.up&&s.key===ce(n))return!0;if(Date.now()>=t)return!1;await new Promise(i=>setTimeout(i,250))}}l(da,"serverAnswers");async function gt(){if(ie)return;wt.check();const{sites:e}=g();for(const t of[...j.keys()])B.has(t)||(R(e,t)?Pe(R(e,t))||(c(`${t} no longer has a command; stopping it`),ue(t),j.delete(t)):(c(`${t} left the manifest; stopping it`),ue(t),j.delete(t)));for(const t of[...J.keys()])U(R(e,t))!=="port"&&J.delete(t);for(const[t,n]of Object.entries(e))U(n)==="port"&&J.set(t,await Re(n.port));for(const[t,n]of Object.entries(e)){if(!Pe(n))continue;mt&&ca(t,n);const s=j.get(t);if(s&&s.child&&s.key!==ce(n)){c(`${t} entry changed; restarting it`),ue(t),s.restarts=0,s.nextStart=Date.now()+1500;continue}const i=s?.child&&s.code&&!s.stopping&&!K.has(t)&&!B.has(t)?qe(n):null;if(i&&i!==s.code){if(!(await Ns(n.dir)).length){c(`${t} runs older code than its files; restarting it`),ue(t),Object.assign(s,{restarts:0,nextStart:Date.now()+1500,why:"code"});continue}s.waitedOn!==i&&c(`${t} runs older code than its files; restarts once no builder runs`),s.waitedOn=i}(!s||!s.child&&Date.now()>=(s.nextStart||0))&&la(t,n)}mt=!1;for(const[t,n]of j){const s=R(e,t);s&&(n.up=n.child?await Re(s.port):!1,n.child&&n.up&&Object.assign(n,{state:"up"},n.stopping?{}:{why:void 0}))}}l(gt,"reconcile");const wt=Bs({root:ve(),files:In().files,restart:l(()=>ct("hub"),"restart"),log:c}),ha=setInterval(()=>gt().catch(e=>c("reconcile failed:",e.message)),2e3);gt().catch(e=>c("reconcile failed:",e.message));try{Yt()&&c("settings: migrated the polling windows to quiet hours")}catch(e){c("settings migration failed:",e.message)}const bt=l(async()=>{try{for(const e of await Fs())c(`linear: ${e}`)}catch(e){c("linear fallback failed:",e.message)}},"linearFallback");await bt();const fa=6e4,De=60*6e4,y={running:null,startedAt:0,firstAt:Date.now()+fa,last:null,remaining:null,errorLoggedAt:0,linearErrorLoggedAt:0,slackErrorLoggedAt:0},F=an(),yt=l(()=>y.startedAt?wn({last:new Date(y.startedAt),...F.state(),remaining:y.remaining}):new Date(y.firstAt),"nextPollAt");function St({changed:e,back:t}){e&&x.refresh(),t&&y.startedAt&&!y.running&&Le().catch(n=>c("poll failed:",n.message))}l(St,"presenceChanged");function Le({flush:e=!0}={}){if(y.running)return y.running;const t=new Date;return y.startedAt=t.getTime(),y.running=(async()=>{const n=ee();let s;try{s=await Pn({every:Ze({present:!1,remaining:y.remaining}),manifest:g()})}catch(i){s={at:t.toISOString(),watched:0,failed:[i.message],changes:[],events:[],checked:[],merged:[],linear:{moved:[],failed:[]},rate:null}}y.remaining=s.rate?.remaining??null,s.merged.length&&(await ot({manifest:g(),log:c,routes:s.merged}),c(`merged on ${s.merged.join(", ")}: fetched and refreshing Main`),xe({manifest:g(),routes:s.merged}).catch(i=>c("merge refresh failed:",i.message))),await ot({manifest:g(),log:c});try{const i=await Is({manifest:g()});i.length&&xe({manifest:g(),routes:i}).catch(a=>c("pull refresh failed:",a.message))}catch(i){c("pull failed:",i.message)}try{s.failed.push(...(await En({manifest:g()})).failed)}catch(i){s.failed.push(`incoming PRs: ${i.message}`)}s.failed.length&&Date.now()-y.errorLoggedAt>De&&(y.errorLoggedAt=Date.now(),c(`github check: ${s.failed.length} step${s.failed.length===1?"":"s"} failed, first: ${s.failed[0]} (logged at most hourly)`)),s.changes.length&&c(`github check: ${s.changes.map(i=>`#${i.pr} ${i.changes.join(",")}`).join("; ")} \u2192 ${s.events.map(i=>i.route).join(", ")||"no board"}`);for(const{id:i,state:a}of s.linear.moved)c(`linear: ${i} \u2192 ${a}`);s.linear.failed.length&&Date.now()-y.linearErrorLoggedAt>De&&(y.linearErrorLoggedAt=Date.now(),c(`linear check: ${s.linear.failed.length} step${s.linear.failed.length===1?"":"s"} failed, first: ${s.linear.failed[0]} (logged at most hourly)`));try{s.slack=await ra({boards:D(g())})}catch(i){s.slack={sent:[],failed:[i.message]}}for(const i of s.slack.sent)c(`slack: sent ${i}`);s.slack.failed.length&&Date.now()-y.slackErrorLoggedAt>De&&(y.slackErrorLoggedAt=Date.now(),c(`slack sends: ${s.slack.failed.length} failed, first: ${s.slack.failed[0]} (logged at most hourly)`));try{s.deaf=await Cn({manifest:g()})}catch(i){s.deaf={pushed:[],failed:[i.message]}}(s.deaf.pushed.length||s.deaf.failed.length)&&c(`not listening: pushed ${s.deaf.pushed.join(", ")||"none"}${s.deaf.failed.length?`; ${s.deaf.failed.join("; ")}`:""}`);try{Hn(g())}catch(i){c("could not record link faults:",i.message)}if(s.flushed=null,s.held=pn(t,n)!==null,e&&!s.held)try{const i=await Ge();i&&(s.flushed={count:i.count,sent:i.sent,ok:i.ok},c(`push queue: sent ${i.sent} of ${i.count} queued`))}catch(i){c("push queue flush failed:",i.message)}return y.last=s,s})().finally(()=>{y.running=null}),y.running}l(Le,"runPoll");let kt=null;function pa(){kt||=Rn({settings:ee(),manifest:g()}).then(e=>{e.due.length&&c(`wake ${e.due.join(", ")}: check-github \u2192 ${e.events.map(t=>t.route).join(", ")||"no board"}${e.failed.length?`; ${e.failed.join("; ")}`:""}`)}).catch(e=>c("wake failed:",e.message)).finally(()=>{kt=null})}l(pa,"runWakes");const Ce=new Set,A={running:null};function $t(){if(A.running||B.size||K.size)return;const e=new Set(Ce);Ce.clear(),A.running=Qn({manifest:g(),asked:e}).then(t=>{(t.resumed.length||t.pushed.length||t.failed.length)&&c(`sessions: resumed ${t.resumed.join(", ")||"none"}; pushed ${t.pushed.join(", ")||"none"}${t.failed.length?`; ${t.failed.join("; ")}`:""}`)}).catch(t=>c("session check failed:",t.message)).then(()=>Xn({manifest:g()})).then(t=>{(t.compacted.length||t.failed.length)&&c(`context full: sent /compact to ${t.compacted.join(", ")||"none"}${t.failed.length?`; ${t.failed.join("; ")}`:""}`)}).catch(t=>c("context full check failed:",t.message)).finally(()=>{A.running=null})}l($t,"runSessionCheck"),await Cs(g());const ma=Date.now(),V=new Map;function Z(e){e?(pe.delete(e),x.changed(e)):x.refresh()}l(Z,"promptChanged");async function _t(){const{gone:e,rulesChanged:t}=await G(n=>{const s=on(n,{now:Date.now(),aliveSince:ma}),i=ye(n,Ae());for(const a of s)delete n.prompts[a.id];return{gone:s,rulesChanged:i}});for(const n of e)Z(n.route);t&&x.refresh(),e.length&&c(`prompts: dropped ${e.map(n=>n.id).join(", ")}, whose hook went away`)}l(_t,"prunePrompts");const Ae=l(()=>({running:tt(),boards:D(g())}),"promptContext");async function xt(e){const t=D(g()).find(n=>n.owner?.session&&n.owner.session===e.session_id);if(!t)return null;try{const n=await Gs(st(t.route).repo,t.dir,e);return n&&c(`permissions: /${t.route} was still ${e.hook_event_name==="PermissionDenied"?"refused":"asked about"} ${e.tool_input.command}`),n}catch(n){return c(`permissions: could not note /${t.route}'s refusal: ${n.message}`),null}}l(xt,"noteRefusal");let Q=null,Tt=null;async function he(){return Q?Tt??=Q.catch(()=>{}).then(()=>(Tt=null,he())):(Q=(async()=>$s({gate:te(),resume:l(async e=>{const t=await Ye(D(g()),"usage-resumed",{window:e});for(const n of t.failed)c(n)},"resume")}))().finally(()=>{Q=null}),Q)}l(he,"checkUsageGate"),setInterval(()=>{xe({manifest:g()}).catch(e=>c("try tick failed:",e.message)),he().catch(e=>c("usage gate failed:",e.message));try{js()}catch(e){c("machine check failed:",e.message)}F.check().then(St,e=>c("presence check failed:",e.message)).finally(()=>{!y.running&&Date.now()>=yt().getTime()&&Le().catch(e=>c("poll failed:",e.message))}),cs({listening:l(e=>Dn(e)&&fe().some(t=>t.route===e.route&&t.state==="up"),"listening")}).then(e=>{e.length&&x.refresh()},e=>c("settling starts failed:",e.message)),pa(),re.apply(),_t().catch(e=>c("prompt expiry failed:",e.message)).finally($t)},15e3);function vt(){if(ie)return;ie=!0,clearInterval(ha),re.stop(),c("hub stopping; stopping sites");for(const n of j.values())n.child&&(n.stopping=!0);for(const n of H)de(n,"SIGTERM");let e=!1;Ds().finally(()=>{e=!0});const t=Date.now()+ft;setInterval(()=>{if(!((!e||H.size)&&Date.now()<t)){for(const n of H)c(`pid ${n.pid} ignored SIGTERM; killing it`),de(n,"SIGKILL");process.exit(0)}},50)}l(vt,"shutdown"),process.on("SIGTERM",vt),process.on("SIGINT",vt);const Ot=new Map;function jt(e,t={n:3e3}){let n=0,s;try{s=b.readdirSync(e,{withFileTypes:!0})}catch{return 0}for(const i of s){if(t.n--<=0)break;if(i.name.startsWith(".")||i.name==="node_modules")continue;const a=$.join(e,i.name);if(i.isDirectory())n=Math.max(n,jt(a,t));else try{n=Math.max(n,b.statSync(a).mtimeMs)}catch{}}return n}l(jt,"newestMtime");function ga(e,t){const n=Ot.get(e);if(n&&Date.now()-n.checked<3e4)return n.at;const s=jt(t.dir);return Ot.set(e,{at:s,checked:Date.now()}),s}l(ga,"changedAt");const Pt=l(e=>{try{return typeof e=="string"&&b.statSync(e).isDirectory()}catch{return!1}},"isFolder");function fe(){const{sites:e}=g();return Object.entries(e).sort(([t],[n])=>t.localeCompare(n)).map(([t,n])=>{const s=j.get(t)||{},i=U(n),a=i==="files"?Pt(n.dir)?"up":"down":i==="port"?J.has(t)?J.get(t)?"up":"down":"starting":s.child?s.up?"up":"starting":s.state==="crashed"?"down":s.state||"starting";return{route:t,title:n.title||t,description:n.description||"",icon:n.icon||null,color:n.color||null,port:n.port??null,serves:i,state:a,exitCode:s.exitCode??null,owner:n.owner||null,restart:ps(n),changedAt:ga(t,n)||null,created:n.created||null,log:$.join(ae,t+".log")}})}l(fe,"status");const N=l(e=>String(e??"").replace(/[&<>"']/g,t=>({"&":"&amp;","<":"&lt;",">":"&gt;",'"':"&quot;","'":"&#39;"})[t]),"esc"),pe=new Map,Rt=2e4,wa=1500;async function ba(e){try{const t=await fetch(`http://127.0.0.1:${e}/_claude/needs`,{signal:AbortSignal.timeout(wa)});return t.ok?Jn(await t.json()):null}catch{return null}}l(ba,"fetchNeeds");async function Et(e,{fresh:t=!1}={}){const n=e.filter(i=>i.state==="up"&&i.owner&&i.port);for(const i of n){const a=pe.get(i.route);(t===!0||t?.has?.(i.route)||!a||Date.now()-a.at>=Rt)&&pe.set(i.route,{at:Date.now(),needs:ba(i.port)})}const s=await Promise.all(n.map(i=>pe.get(i.route).needs));return new Map(n.map((i,a)=>[i.route,s[a]]))}l(Et,"waitingCounts");const Dt=l(e=>[...e.values()].reduce((t,n)=>t+(n?.count||0),0)+Ke(null).length,"badgeTotal"),Ie=new Map;function ya(e){const t=new Map,n=new Map,s={running:tt(),wakes:et(Ln,{})};for(const i of e)try{const a=Xe(i,s);n.set(i.route,a);let p=Ie.get(i.route);(!p||p.listen!==a||Date.now()-p.at>=Rt)&&(p={at:Date.now(),listen:a,links:Un(i,{listen:a})},Ie.set(i.route,p)),t.set(i.route,p.links)}catch(a){c(`links of ${i.route} failed:`,a.message),t.set(i.route,[])}return{links:t,listens:n}}l(ya,"boardLinks");async function X({fresh:e=!1}={}){const t=new Date,n=fe(),s=await Et(n,{fresh:e}),i=g(),a=D(i),{links:p,listens:h}=ya(a),u=Fn(),_=new Map(a.map(o=>[o.route,_s(o.owner?.session)]).filter(([,o])=>o)),k=Yn(i,t.getTime(),is,o=>h.get(o)==="listening"&&n.some(d=>d.route===o&&d.state==="up")),S=new Set(k.map(o=>o.route)),m=new Map(a.filter(o=>Os(o.dir)).map(o=>[o.route,te({dir:o.dir})])),w=Kn(n,s,new Set(a.map(o=>o.route)),h,_).filter(o=>!S.has(o.route)).map(o=>m.has(o.route)?{...o,pause:m.get(o.route)}:o),O=ee(),C=fn(t,O);return{data:{code:u.running??null,version:ut,total:Dt(s),start:Vn(w),sites:w,hub:Zn(w,p,Bn(u)),home:b.realpathSync(Fe.homedir()),starting:k,usage:ia({codexShare:We(ne())}),pause:te(),notifications:C?C.why==="off"?{state:"off",text:"Off"}:{state:"held",text:yn(mn(C,t))}:{state:"on",text:"On"},away:!F.state().present},links:p,rows:n,code:u}}l(X,"sidebarState");function Lt(e,t,n,s,i){let a=Buffer.isBuffer(i)?i:Buffer.from(i);const p={...s,vary:[s.vary,"accept-encoding"].filter(Boolean).join(", ")};a.length>=1024&&/\bgzip\b/.test(e.headers["accept-encoding"]||"")&&(a=Wt.gzipSync(a),p["content-encoding"]="gzip"),p["content-length"]=a.length,t.writeHead(n,p),t.end(e.method==="HEAD"?void 0:a)}l(Lt,"sendText");async function Sa(e,t=null){const n=t?t.sidebar((await X()).data):(await X()).data,s=Ne.createContext({});for(const i of["ui-progress-bar.js","ui-page-header.js","ui-sidebar.js"])Ne.runInContext(b.readFileSync($.join(_e,i),"utf8"),s);return ys({sidebar:s.BoardUI.sidebar(n,{here:e}),sidebarData:JSON.stringify(n).replace(/</g,"\\u003c"),touchIcon:me(180)})}l(Sa,"shellPage");const Ct=Hs({root:ve(),run:ka,log:c,pulled:l(()=>wt.restartNow(),"pulled")});function ka(){const e=$.join(ae,"kickflip-update.log");b.writeFileSync(e,`${new Date().toISOString()} kickflip update
5
+ `),b.closeSync(s),Object.assign(n,{child:null,key:ce(t),state:"crashed",exitCode:null,nextStart:Date.now()+pt(n.restarts++)}),j.set(e,n);return}b.closeSync(s),b.writeFileSync(le(e),String(h.pid)),Object.assign(n,{child:h,key:ce(t),state:"starting",since:Date.now(),exitCode:null,up:!1}),j.set(e,n),c(`started ${e} pid ${h.pid}`),h.pid&&H.add(h);const u=l((_,k)=>{if(H.delete(h),n.child!==h)return;try{b.unlinkSync(le(e))}catch{}Date.now()-n.since>6e4&&(n.restarts=0),Object.assign(n,{child:null,state:n.stopping?"stopped":"crashed",exitCode:_??k,nextStart:Date.now()+pt(n.restarts++),why:n.stopping?n.why:void 0}),n.stopping=!1,c(`${e} exited (${_??k})`)},"ended");h.on("error",_=>{c(`${e} spawn error: ${_.message}`),h.pid||u(_.code,null)}),h.on("exit",u)}l(ua,"start");const pt=l(e=>Math.min(6e4,1e3*2**Math.min(e,6)),"backoff");function ue(e){const t=j.get(e);if(!t||!t.child){j.delete(e);return}t.stopping=!0;const n=t.child;de(n,"SIGTERM"),setTimeout(()=>{H.has(n)&&de(n,"SIGKILL")},ft)}l(ue,"stop");const de=l((e,t)=>{try{process.kill(-e.pid,t)}catch{}},"signal");function Re(e){return new Promise(t=>{const n=Gt.connect({port:e,host:"127.0.0.1"}),s=l(i=>{n.destroy(),t(i)},"done");n.setTimeout(800,()=>s(!1)),n.on("connect",()=>s(!0)),n.on("error",()=>s(!1))})}l(Re,"probe");let mt=!0;const B=new Set;let Ee=!1;const J=new Set,da=3e4;async function ha(e){const t=Date.now()+da;for(;;){const n=R(g().sites,e),s=j.get(e);if(n&&s?.child&&s.up&&s.key===ce(n))return!0;if(Date.now()>=t)return!1;await new Promise(i=>setTimeout(i,250))}}l(ha,"serverAnswers");async function gt(){if(ie)return;wt&&bt.check();const{sites:e}=g();for(const t of[...j.keys()])B.has(t)||(R(e,t)?Pe(R(e,t))||(c(`${t} no longer has a command; stopping it`),ue(t),j.delete(t)):(c(`${t} left the manifest; stopping it`),ue(t),j.delete(t)));for(const t of[...W.keys()])U(R(e,t))!=="port"&&W.delete(t);for(const[t,n]of Object.entries(e))U(n)==="port"&&W.set(t,await Re(n.port));for(const[t,n]of Object.entries(e)){if(!Pe(n))continue;mt&&la(t,n);const s=j.get(t);if(s&&s.child&&s.key!==ce(n)){c(`${t} entry changed; restarting it`),ue(t),s.restarts=0,s.nextStart=Date.now()+1500;continue}const i=wt&&s?.child&&s.code&&!s.stopping&&!J.has(t)&&!B.has(t)?qe(n):null;if(i&&i!==s.code){if(!(await Gs(n.dir)).length){c(`${t} runs older code than its files; restarting it`),ue(t),Object.assign(s,{restarts:0,nextStart:Date.now()+1500,why:"code"});continue}s.waitedOn!==i&&c(`${t} runs older code than its files; restarts once no builder runs`),s.waitedOn=i}(!s||!s.child&&Date.now()>=(s.nextStart||0))&&ua(t,n)}mt=!1;for(const[t,n]of j){const s=R(e,t);s&&(n.up=n.child?await Re(s.port):!1,n.child&&n.up&&Object.assign(n,{state:"up"},n.stopping?{}:{why:void 0}))}}l(gt,"reconcile");const wt=process.env.KICKFLIP_CODE_RESTARTS!=="off",bt=Fs({root:ve(),files:Mn().files,restart:l(()=>ct("hub"),"restart"),log:c}),fa=setInterval(()=>gt().catch(e=>c("reconcile failed:",e.message)),2e3);gt().catch(e=>c("reconcile failed:",e.message));try{qt()&&c("settings: migrated the polling windows to quiet hours")}catch(e){c("settings migration failed:",e.message)}const yt=l(async()=>{try{for(const e of await Ns())c(`linear: ${e}`)}catch(e){c("linear fallback failed:",e.message)}},"linearFallback");await yt();const pa=6e4,De=60*6e4,y={running:null,startedAt:0,firstAt:Date.now()+pa,last:null,remaining:null,errorLoggedAt:0,linearErrorLoggedAt:0,slackErrorLoggedAt:0},F=rn(),St=l(()=>y.startedAt?bn({last:new Date(y.startedAt),...F.state(),remaining:y.remaining}):new Date(y.firstAt),"nextPollAt");function kt({changed:e,back:t}){e&&x.refresh(),t&&y.startedAt&&!y.running&&Le().catch(n=>c("poll failed:",n.message))}l(kt,"presenceChanged");function Le({flush:e=!0}={}){if(y.running)return y.running;const t=new Date;return y.startedAt=t.getTime(),y.running=(async()=>{const n=ee();let s;try{s=await Rn({every:Ze({present:!1,remaining:y.remaining}),manifest:g()})}catch(i){s={at:t.toISOString(),watched:0,failed:[i.message],changes:[],events:[],checked:[],merged:[],linear:{moved:[],failed:[]},rate:null}}y.remaining=s.rate?.remaining??null,s.merged.length&&(await ot({manifest:g(),log:c,routes:s.merged}),c(`merged on ${s.merged.join(", ")}: fetched and refreshing Main`),xe({manifest:g(),routes:s.merged}).catch(i=>c("merge refresh failed:",i.message))),await ot({manifest:g(),log:c});try{const i=await Ms({manifest:g()});i.length&&xe({manifest:g(),routes:i}).catch(a=>c("pull refresh failed:",a.message))}catch(i){c("pull failed:",i.message)}try{s.failed.push(...(await Dn({manifest:g()})).failed)}catch(i){s.failed.push(`incoming PRs: ${i.message}`)}s.failed.length&&Date.now()-y.errorLoggedAt>De&&(y.errorLoggedAt=Date.now(),c(`github check: ${s.failed.length} step${s.failed.length===1?"":"s"} failed, first: ${s.failed[0]} (logged at most hourly)`)),s.changes.length&&c(`github check: ${s.changes.map(i=>`#${i.pr} ${i.changes.join(",")}`).join("; ")} \u2192 ${s.events.map(i=>i.route).join(", ")||"no board"}`);for(const{id:i,state:a}of s.linear.moved)c(`linear: ${i} \u2192 ${a}`);s.linear.failed.length&&Date.now()-y.linearErrorLoggedAt>De&&(y.linearErrorLoggedAt=Date.now(),c(`linear check: ${s.linear.failed.length} step${s.linear.failed.length===1?"":"s"} failed, first: ${s.linear.failed[0]} (logged at most hourly)`));try{s.slack=await oa({boards:D(g())})}catch(i){s.slack={sent:[],failed:[i.message]}}for(const i of s.slack.sent)c(`slack: sent ${i}`);s.slack.failed.length&&Date.now()-y.slackErrorLoggedAt>De&&(y.slackErrorLoggedAt=Date.now(),c(`slack sends: ${s.slack.failed.length} failed, first: ${s.slack.failed[0]} (logged at most hourly)`));try{s.deaf=await An({manifest:g()})}catch(i){s.deaf={pushed:[],failed:[i.message]}}(s.deaf.pushed.length||s.deaf.failed.length)&&c(`not listening: pushed ${s.deaf.pushed.join(", ")||"none"}${s.deaf.failed.length?`; ${s.deaf.failed.join("; ")}`:""}`);try{Un(g())}catch(i){c("could not record link faults:",i.message)}if(s.flushed=null,s.held=mn(t,n)!==null,e&&!s.held)try{const i=await Ge();i&&(s.flushed={count:i.count,sent:i.sent,ok:i.ok},c(`push queue: sent ${i.sent} of ${i.count} queued`))}catch(i){c("push queue flush failed:",i.message)}return y.last=s,s})().finally(()=>{y.running=null}),y.running}l(Le,"runPoll");let $t=null;function ma(){$t||=En({settings:ee(),manifest:g()}).then(e=>{e.due.length&&c(`wake ${e.due.join(", ")}: check-github \u2192 ${e.events.map(t=>t.route).join(", ")||"no board"}${e.failed.length?`; ${e.failed.join("; ")}`:""}`)}).catch(e=>c("wake failed:",e.message)).finally(()=>{$t=null})}l(ma,"runWakes");const Ce=new Set,A={running:null};function _t(){if(A.running||B.size||J.size)return;const e=new Set(Ce);Ce.clear(),A.running=Xn({manifest:g(),asked:e}).then(t=>{(t.resumed.length||t.pushed.length||t.failed.length)&&c(`sessions: resumed ${t.resumed.join(", ")||"none"}; pushed ${t.pushed.join(", ")||"none"}${t.failed.length?`; ${t.failed.join("; ")}`:""}`)}).catch(t=>c("session check failed:",t.message)).then(()=>Yn({manifest:g()})).then(t=>{(t.compacted.length||t.failed.length)&&c(`context full: sent /compact to ${t.compacted.join(", ")||"none"}${t.failed.length?`; ${t.failed.join("; ")}`:""}`)}).catch(t=>c("context full check failed:",t.message)).finally(()=>{A.running=null})}l(_t,"runSessionCheck"),await As(g());const ga=Date.now(),V=new Map;function Z(e){e?(pe.delete(e),x.changed(e)):x.refresh()}l(Z,"promptChanged");async function xt(){const{gone:e,rulesChanged:t}=await G(n=>{const s=cn(n,{now:Date.now(),aliveSince:ga}),i=ye(n,Ae());for(const a of s)delete n.prompts[a.id];return{gone:s,rulesChanged:i}});for(const n of e)Z(n.route);t&&x.refresh(),e.length&&c(`prompts: dropped ${e.map(n=>n.id).join(", ")}, whose hook went away`)}l(xt,"prunePrompts");const Ae=l(()=>({running:tt(),boards:D(g())}),"promptContext");async function Tt(e){const t=D(g()).find(n=>n.owner?.session&&n.owner.session===e.session_id);if(!t)return null;try{const n=await zs(st(t.route).repo,t.dir,e);return n&&c(`permissions: /${t.route} was still ${e.hook_event_name==="PermissionDenied"?"refused":"asked about"} ${e.tool_input.command}`),n}catch(n){return c(`permissions: could not note /${t.route}'s refusal: ${n.message}`),null}}l(Tt,"noteRefusal");let Q=null,vt=null;async function he(){return Q?vt??=Q.catch(()=>{}).then(()=>(vt=null,he())):(Q=(async()=>_s({gate:te(),resume:l(async e=>{const t=await Ye(D(g()),"usage-resumed",{window:e});for(const n of t.failed)c(n)},"resume")}))().finally(()=>{Q=null}),Q)}l(he,"checkUsageGate"),setInterval(()=>{xe({manifest:g()}).catch(e=>c("try tick failed:",e.message)),he().catch(e=>c("usage gate failed:",e.message));try{Ps()}catch(e){c("machine check failed:",e.message)}F.check().then(kt,e=>c("presence check failed:",e.message)).finally(()=>{!y.running&&Date.now()>=St().getTime()&&Le().catch(e=>c("poll failed:",e.message))}),ls({listening:l(e=>Ln(e)&&fe().some(t=>t.route===e.route&&t.state==="up"),"listening")}).then(e=>{e.length&&x.refresh()},e=>c("settling starts failed:",e.message)),ma(),re.apply(),xt().catch(e=>c("prompt expiry failed:",e.message)).finally(_t)},15e3);function Ot(){if(ie)return;ie=!0,clearInterval(fa),re.stop(),c("hub stopping; stopping sites");for(const n of j.values())n.child&&(n.stopping=!0);for(const n of H)de(n,"SIGTERM");let e=!1;Ls().finally(()=>{e=!0});const t=Date.now()+ft;setInterval(()=>{if(!((!e||H.size)&&Date.now()<t)){for(const n of H)c(`pid ${n.pid} ignored SIGTERM; killing it`),de(n,"SIGKILL");process.exit(0)}},50)}l(Ot,"shutdown"),process.on("SIGTERM",Ot),process.on("SIGINT",Ot);const jt=new Map;function Pt(e,t={n:3e3}){let n=0,s;try{s=b.readdirSync(e,{withFileTypes:!0})}catch{return 0}for(const i of s){if(t.n--<=0)break;if(i.name.startsWith(".")||i.name==="node_modules")continue;const a=$.join(e,i.name);if(i.isDirectory())n=Math.max(n,Pt(a,t));else try{n=Math.max(n,b.statSync(a).mtimeMs)}catch{}}return n}l(Pt,"newestMtime");function wa(e,t){const n=jt.get(e);if(n&&Date.now()-n.checked<3e4)return n.at;const s=Pt(t.dir);return jt.set(e,{at:s,checked:Date.now()}),s}l(wa,"changedAt");const Rt=l(e=>{try{return typeof e=="string"&&b.statSync(e).isDirectory()}catch{return!1}},"isFolder");function fe(){const{sites:e}=g();return Object.entries(e).sort(([t],[n])=>t.localeCompare(n)).map(([t,n])=>{const s=j.get(t)||{},i=U(n),a=i==="files"?Rt(n.dir)?"up":"down":i==="port"?W.has(t)?W.get(t)?"up":"down":"starting":s.child?s.up?"up":"starting":s.state==="crashed"?"down":s.state||"starting";return{route:t,title:n.title||t,description:n.description||"",icon:n.icon||null,color:n.color||null,port:n.port??null,serves:i,state:a,exitCode:s.exitCode??null,owner:n.owner||null,restart:ms(n),changedAt:wa(t,n)||null,created:n.created||null,log:$.join(ae,t+".log")}})}l(fe,"status");const N=l(e=>String(e??"").replace(/[&<>"']/g,t=>({"&":"&amp;","<":"&lt;",">":"&gt;",'"':"&quot;","'":"&#39;"})[t]),"esc"),pe=new Map,Et=2e4,ba=1500;async function ya(e){try{const t=await fetch(`http://127.0.0.1:${e}/_claude/needs`,{signal:AbortSignal.timeout(ba)});return t.ok?Jn(await t.json()):null}catch{return null}}l(ya,"fetchNeeds");async function Dt(e,{fresh:t=!1}={}){const n=e.filter(i=>i.state==="up"&&i.owner&&i.port);for(const i of n){const a=pe.get(i.route);(t===!0||t?.has?.(i.route)||!a||Date.now()-a.at>=Et)&&pe.set(i.route,{at:Date.now(),needs:ya(i.port)})}const s=await Promise.all(n.map(i=>pe.get(i.route).needs));return new Map(n.map((i,a)=>[i.route,s[a]]))}l(Dt,"waitingCounts");const Lt=l(e=>[...e.values()].reduce((t,n)=>t+(n?.count||0),0)+Je(null).length,"badgeTotal"),Ie=new Map;function Sa(e){const t=new Map,n=new Map,s={running:tt(),wakes:et(Cn,{})};for(const i of e)try{const a=Xe(i,s);n.set(i.route,a);let p=Ie.get(i.route);(!p||p.listen!==a||Date.now()-p.at>=Et)&&(p={at:Date.now(),listen:a,links:Bn(i,{listen:a})},Ie.set(i.route,p)),t.set(i.route,p.links)}catch(a){c(`links of ${i.route} failed:`,a.message),t.set(i.route,[])}return{links:t,listens:n}}l(Sa,"boardLinks");async function X({fresh:e=!1}={}){const t=new Date,n=fe(),s=await Dt(n,{fresh:e}),i=g(),a=D(i),{links:p,listens:h}=Sa(a),u=Nn(),_=new Map(a.map(o=>[o.route,xs(o.owner?.session)]).filter(([,o])=>o)),k=qn(i,t.getTime(),cs,o=>h.get(o)==="listening"&&n.some(d=>d.route===o&&d.state==="up")),S=new Set(k.map(o=>o.route)),m=new Map(a.filter(o=>js(o.dir)).map(o=>[o.route,te({dir:o.dir})])),w=Vn(n,s,new Set(a.map(o=>o.route)),h,_).filter(o=>!S.has(o.route)).map(o=>m.has(o.route)?{...o,pause:m.get(o.route)}:o),O=ee(),C=pn(t,O);return{data:{code:u.running??null,version:ut,total:Lt(s),start:Zn(w),sites:w,hub:Qn(w,p,Fn(u)),home:b.realpathSync(Fe.homedir()),starting:k,usage:(({start:o,...d})=>d)(ca({codexShare:Ke(ne())})),pause:te(),notifications:C?C.why==="off"?{state:"off",text:"Off"}:{state:"held",text:Sn(gn(C,t))}:{state:"on",text:"On"},away:!F.state().present},links:p,rows:n,code:u}}l(X,"sidebarState");function Ct(e,t,n,s,i){let a=Buffer.isBuffer(i)?i:Buffer.from(i);const p={...s,vary:[s.vary,"accept-encoding"].filter(Boolean).join(", ")};a.length>=1024&&/\bgzip\b/.test(e.headers["accept-encoding"]||"")&&(a=Wt.gzipSync(a),p["content-encoding"]="gzip"),p["content-length"]=a.length,t.writeHead(n,p),t.end(e.method==="HEAD"?void 0:a)}l(Ct,"sendText");async function ka(e,t=null){const n=t?t.sidebar((await X()).data):(await X()).data,s=Ne.createContext({});for(const i of["ui-progress-bar.js","ui-page-header.js","ui-sidebar.js"])Ne.runInContext(b.readFileSync($.join(_e,i),"utf8"),s);return Ss({sidebar:s.BoardUI.sidebar(n,{here:e}),sidebarData:JSON.stringify(n).replace(/</g,"\\u003c"),touchIcon:me(180)})}l(ka,"shellPage");const At=Us({root:ve(),run:$a,log:c,pulled:l(()=>bt.restartNow(),"pulled")});function $a(){const e=$.join(ae,"kickflip-update.log");b.writeFileSync(e,`${new Date().toISOString()} kickflip update
6
6
  `);const t=b.openSync(e,"a");return c("pulling the new kickflip"),new Promise(n=>{const s=Be(process.execPath,[$.join(ve(),"bin","kickflip.mjs"),"update","--no-restart"],{detached:!0,stdio:["ignore",t,t]});b.closeSync(t),s.unref(),s.on("error",i=>n({ok:!1,error:i.message})),s.on("exit",i=>{const a=b.readFileSync(e,"utf8").trim().split(`
7
- `).at(-1);c(`kickflip update exited ${i}: ${a}`),n(i===0?{ok:!0}:{ok:!1,error:a.replace(/^kickflip: /,"")})})})}l(ka,"runUpdate"),Ct.check(),setInterval(()=>Ct.check(),Us);const x=Ss({targets:l(()=>Object.entries(g().sites).filter(([,e])=>Se(e)&&e.port).map(([e,t])=>({route:e,port:t.port})),"targets"),watched:l(()=>D(g()).map(({route:e,dir:t})=>({route:e,dir:t})),"watched"),sidebar:l(async({fresh:e})=>(await X({fresh:e})).data,"sidebar"),log:c});function me(e){let t="0";try{t=Math.floor(b.statSync($.join(z,`icon-${e}.png`)).mtimeMs).toString(36)}catch{}return`/_hub/icon-${e}.png?v=${t}`}l(me,"iconUrl");const $a=l(()=>JSON.stringify({name:"Claude sites",short_name:"Claude",start_url:"/",scope:"/",display:"standalone",background_color:"#121514",theme_color:"#121514",icons:[{src:me(192),sizes:"192x192",type:"image/png"},{src:me(512),sizes:"512x512",type:"image/png"}]}),"manifestJson");async function E(e,t=16*1024){let n="";for await(const s of e)if(n+=s,n.length>t)throw Object.assign(new Error("too large"),{status:413});return n}l(E,"readBody");async function v(e,t){if(!$e(e.headers))throw Object.assign(new Error("Send the body as application/json."),{status:403});const n=await E(e,t);return n?JSON.parse(n):{}}l(v,"readJSONBody");const At=l((e,t="")=>`<!doctype html><html lang="en" class="hub-view"><head><meta charset="utf-8"><meta name="viewport" content="width=device-width, initial-scale=1, viewport-fit=cover">
7
+ `).at(-1);c(`kickflip update exited ${i}: ${a}`),n(i===0?{ok:!0}:{ok:!1,error:a.replace(/^kickflip: /,"")})})})}l($a,"runUpdate"),At.check(),setInterval(()=>At.check(),Bs);const x=ks({targets:l(()=>Object.entries(g().sites).filter(([,e])=>Se(e)&&e.port).map(([e,t])=>({route:e,port:t.port})),"targets"),watched:l(()=>D(g()).map(({route:e,dir:t})=>({route:e,dir:t})),"watched"),sidebar:l(async({fresh:e})=>(await X({fresh:e})).data,"sidebar"),log:c});function me(e){let t="0";try{t=Math.floor(b.statSync($.join(z,`icon-${e}.png`)).mtimeMs).toString(36)}catch{}return`/_hub/icon-${e}.png?v=${t}`}l(me,"iconUrl");const _a=l(()=>JSON.stringify({name:"Claude sites",short_name:"Claude",start_url:"/",scope:"/",display:"standalone",background_color:"#121514",theme_color:"#121514",icons:[{src:me(192),sizes:"192x192",type:"image/png"},{src:me(512),sizes:"512x512",type:"image/png"}]}),"manifestJson");async function E(e,t=16*1024){let n="";for await(const s of e)if(n+=s,n.length>t)throw Object.assign(new Error("too large"),{status:413});return n}l(E,"readBody");async function v(e,t){if(!$e(e.headers))throw Object.assign(new Error("Send the body as application/json."),{status:403});const n=await E(e,t);return n?JSON.parse(n):{}}l(v,"readJSONBody");const It=l((e,t="")=>`<!doctype html><html lang="en" class="hub-view"><head><meta charset="utf-8"><meta name="viewport" content="width=device-width, initial-scale=1, viewport-fit=cover">
8
8
  <title>${N(e)}</title><link rel="manifest" href="/manifest.webmanifest"><link rel="icon" type="image/png" href="/_hub/kickflip.png"><link rel="apple-touch-icon" href="${me(180)}">
9
- <meta name="apple-mobile-web-app-title" content="Claude">${t}<link rel="stylesheet" href="/_hub/colors.css?v=${at()}"><link rel="stylesheet" href="/_hub/hub.css"></head><body><main>`,"head"),_a=l(e=>/\bapplication\/json\b/.test(e.headers.accept||""),"wantsJSON"),xa=l(()=>({running:!!y.running,nextAt:yt(),last:y.last,present:F.state().present,every:Ze({present:F.state().present,remaining:y.remaining}),low:y.remaining!==null&&y.remaining<bn}),"pollState");async function ge(e){await _t();const t=new Date,{data:n,links:s,rows:i,code:a}=await X();return{...Sn({now:t,st:ze(t),poll:xa(),sidebar:n,rows:i,links:s,rules:ke(),integrations:await Ps.rows(),code:a,version:ut,host:e.headers["x-forwarded-host"]||e.headers.host||"",codexShare:We(ne()),claudeModel:tn(),codex:ne(),awake:Qe(t),remembered:new Map(D(g()).map(p=>[p.route,Ve(p.route)]))}),prompts:Ke(null).map(un),remembered:Ve(null)}}l(ge,"hubData");const I=l(async()=>{const e=new Date,t={until:Qe(e),unsupported:On()},n=te({now:e}),s=n.manual==="hub"?jn(n.resets_at,e):null;return{...kn({now:e,st:ze(e),rules:ke(),linear:se(),slack:na(),awake:t,pausedUntil:s}),closed:ds()}},"settingsData"),P=l((e,t)=>{e.writeHead(303,{location:t,"cache-control":"no-store"}),e.end()},"redirect");async function Ta(e,t,n,s=null){const i=l((r,o,d,f={})=>{t.writeHead(r,{"content-type":o,"cache-control":"no-store",...f}),t.end(d)},"send"),a=l((r,o)=>{const d=Mn();return i(r,"application/json",JSON.stringify(o),d?{"x-hub-code":d}:{})},"sendJSON"),p=l(({route:r,started:o})=>(o.then(({error:d})=>{d&&c(`new board /${r} didn't start: ${d}`),x.refresh()}),x.refresh(),a(200,{route:r})),"starting"),h=_a(e),u=n.pathname,_=/^\/_hub\/close-board\/([a-z0-9-]+)$/.exec(u);if(_&&e.method==="GET")try{return a(200,await ls(_[1]))}catch(r){return a(r.status||400,{error:r.message})}if(["/_hub/close-board","/_hub/reopen-board"].includes(u)&&e.method==="POST"){let r;try{const o=await v(e);if(r=o.route,u==="/_hub/reopen-board"){const T=await hs(r);return x.refresh(),a(200,T)}if(B.has(r))return a(409,{error:"That board is already closing."});B.add(r);const d=l(()=>B.delete(r),"release");t.once("finish",d),t.once("close",d),await A.running;const f=await us(r,o.name);return x.refresh(),a(200,f)}catch(o){return a(o.status||400,{error:o.message})}}if(u==="/_hub/restart-board"&&e.method==="POST"){let r;try{if(r=(await v(e)).route,K.has(r))return a(409,{error:"That board is already restarting."});K.add(r),await A.running;const o=await fs(r,{serverUp:da,portUp:Re,onStarting:l(d=>c(`restart /${r}: ${d}\u2019s session is back, its server is still starting`),"onStarting")});return c(`restarted /${r}: server answers, session ${o.session} resumed`),a(200,o)}catch(o){return r&&c(`restart /${r} failed: ${o.message}`),a(o.status||400,{error:o.message})}finally{K.delete(r),x.refresh()}}if(u==="/_hub/restart-hub"&&e.method==="POST")try{return await v(e),Ee?a(409,{error:"The hub is already restarting."}):(Ee=!0,t.once("finish",()=>{c("restarting the hub from the hub page");const r=ct("hub");r&&(Ee=!1,c(`hub restart failed: ${r}`))}),a(200,{restarting:!0,command:Ys().restartCommand}))}catch(r){return a(r.status||400,{error:r.message})}if(u==="/_hub/presence"&&e.method==="POST")return await v(e),St(F.beat()),t.writeHead(204,{"cache-control":"no-store"}),t.end();if(u==="/_hub/wake"&&e.method==="POST")try{const{route:r}=await v(e),o=D(g()).find(d=>d.route===r);return o?(Ce.add(r),await A.running,$t(),await A.running,Ie.clear(),x.changed(r),a(200,{route:r,listen:Xe(o)})):a(404,{error:"That board is not open."})}catch(r){return a(r.status||400,{error:r.message})}if(u==="/_hub/repos"&&e.method==="GET")try{return a(200,await M.list(g(),{fresh:n.searchParams.get("fresh")==="1"}))}catch(r){return a(r.status||502,{error:r.message})}if(u==="/_hub/branches"&&e.method==="GET")try{return a(200,await M.branches(n.searchParams.get("github")??n.searchParams.get("remote")))}catch(r){return a(r.status||502,{error:r.message})}if(u==="/_hub/repo"&&e.method==="GET")return es(n,{repos:M,manifest:g(),sendJSON:a});const k=/^\/_hub\/board-repo\/([a-z0-9-]+)$/.exec(u);if(k&&["GET","POST"].includes(e.method))try{const r=e.method==="GET"?st(k[1]):await ts(k[1],await v(e),{repos:M});return e.method==="POST"&&x.refresh(),a(200,r)}catch(r){return a(r.status||400,{error:r.message})}if(u==="/_hub/new-board"&&e.method==="POST"){if(!$e(e.headers))return i(403,"text/plain; charset=utf-8","Send name, github, branch and purpose as application/json.");let r;try{r=await v(e)}catch{return i(400,"text/plain; charset=utf-8","Send name, github, branch and purpose as JSON.")}try{return p(await as(r,{repos:M,changed:l(()=>x.refresh(),"changed")}))}catch(o){if(o.status)return i(o.status,"text/plain; charset=utf-8",o.message);throw o}}const S=/^\/_hub\/new-board\/([a-z0-9-]+)\/retry$/.exec(u);if(S&&e.method==="POST"){if(!$e(e.headers))return i(403,"text/plain; charset=utf-8","Send the retry as application/json.");try{return p(await rs(S[1],{repos:M,changed:l(()=>x.refresh(),"changed")}))}catch(r){if(r.status)return i(r.status,"text/plain; charset=utf-8",r.message);throw r}}const m=/^\/_hub\/new-board\/([a-z0-9-]+)$/.exec(u);if(m&&e.method==="DELETE")try{const r=await os(m[1]);return x.refresh(),a(200,r)}catch(r){if(r.status)return i(r.status,"text/plain; charset=utf-8",r.message);throw r}if((u==="/_hub/build"||u==="/_hub/pull")&&e.method==="POST"){if(ie)return a(503,{error:"The hub is stopping. Try again after it restarts."});const r=["127.0.0.1","::1","::ffff:127.0.0.1"],o=(e.headers.host||"").replace(/:\d+$/,"");if(!r.includes(e.socket.remoteAddress)||!["127.0.0.1","localhost","[::1]"].includes(o)||e.headers.origin||Object.keys(e.headers).some(f=>f==="forwarded"||f.startsWith("x-forwarded-")))return a(403,{error:`${u==="/_hub/pull"?"Pulls":"Builds"} can only be started from the host over loopback.`});let d;try{d=await v(e)}catch(f){return a(f.status||400,{error:u==="/_hub/pull"?"Send the board route as JSON.":"Send the board route and card key as JSON."})}try{return u==="/_hub/pull"?a(200,await As(d,{manifest:g()})):a(202,await Es(d,{manifest:g(),log:c}))}catch(f){if(f instanceof Rs)return a(f.status,{error:f.message});throw f}}if(u==="/sw.js")return i(200,"text/javascript; charset=utf-8",b.readFileSync($.join(z,"sw.js")),{"service-worker-allowed":"/"});if(u==="/manifest.webmanifest")return i(200,"application/manifest+json",$a());const w=/^\/_hub\/icon-(180|192|512)\.png$/.exec(u);if(w)try{return i(200,"image/png",b.readFileSync($.join(z,`icon-${w[1]}.png`)),{"cache-control":"max-age=86400"})}catch{t.writeHead(404).end();return}if(u==="/_hub/colors.css"){const r=n.searchParams.get("v")===at()?"public, max-age=31536000, immutable":"no-cache";try{return i(200,"text/css; charset=utf-8",b.readFileSync(oa),{"cache-control":r})}catch{t.writeHead(404).end();return}}if(u==="/_hub/kickflip.png")try{return i(200,"image/png",b.readFileSync($.join(_e,"kickflip.png")),{"cache-control":"max-age=86400"})}catch{t.writeHead(404).end();return}if(u==="/_hub/hub.css")return i(200,"text/css; charset=utf-8",b.readFileSync($.join(_e,"hub.css")));if(u==="/_hub/sidebar.json"){const{data:r}=await X({fresh:n.searchParams.get("fresh")==="1"});return a(200,s?s.sidebar(r):r)}if(u==="/_hub/stream"&&e.method==="GET"){if(s){const r={login:s.login,routes:s.routes,res:t};we.add(r),e.on("close",()=>we.delete(r)),e.on("close",Y.watching(s.login))}x.handle(e,t,s&&{routes:s.routes,view:s.sidebar});return}const O=/^\/_hub\/share\/([a-z0-9-]+)$/.exec(u);if(O&&e.method==="GET"){const r=R(g().sites,O[1]);if(!Se(r))return a(404,{error:"That board is not open."});const o=Ks(et($.join(r.dir,"board.json"),{})),d=o.map(L=>L.login),f=(await Te.node())?.dnsName||e.headers["x-forwarded-host"],T=Y.people(O[1],d).map((L,q)=>({...L,level:o[q].level}));return a(200,{people:T,refused:Y.refusals(O[1],d),link:f?`https://${f}/${O[1]}/`:null})}if(u.startsWith("/_hub/app/")){const r=gs(u.slice(10));if(!r){t.writeHead(404).end();return}const o=$.extname(r),d={".js":"text/javascript; charset=utf-8",".css":"text/css; charset=utf-8",".png":"image/png"}[o],f=n.searchParams.get("v")===bs(r)?"public, max-age=31536000, immutable":"no-cache";return o===".png"?i(200,d,b.readFileSync(r),{"cache-control":f}):Lt(e,t,200,{"content-type":d,"cache-control":f},b.readFileSync(r))}if(u==="/_hub/api/hub")return a(200,await ge(e));if(u==="/_hub/api/settings")return a(200,await I());if(u==="/_hub/push/key")return a(200,{key:Jt()});if(u==="/_hub/push/subscribe"&&e.method==="POST"){const r=e.headers["x-forwarded-host"]||e.headers.host;return a(200,Kt(await v(e),`https://${r}/`,e.headers["user-agent"]))}if(u==="/_hub/push/forget"&&e.method==="POST")return a(200,{count:Zt((await v(e))?.id)});if(u==="/_hub/push/unsubscribe"&&e.method==="POST")return a(200,{count:Vt((await v(e))?.endpoint)});if(u==="/_hub/push/test"&&e.method==="POST"){const r=await Qt({title:"Claude",body:"Test notification from the hub.",url:"/",tag:"hub-test"});return a(200,{sent:r.filter(o=>o.status>=200&&o.status<300).length,results:r})}if(u==="/_hub/settings/notifications"&&e.method==="POST"){const r=new URLSearchParams(await E(e)),o=r.get("set"),d=o==="on"?{on:r.get("value")==="on"}:o==="weekends"?{weekends_off:r.get("value")==="on"}:r.has("snooze")?{snooze:r.get("snooze")}:null,f="Pass set=on or set=weekends with value=on, or snooze.";if(!d)return h?a(400,{error:f}):i(400,"text/plain; charset=utf-8",f);let T;try{T=await qt(d)}catch(L){if(L.status!==400)throw L;return h?a(400,{error:_n}):P(t,"/_hub/?refused=snooze#notifications")}return T.dropped&&c(`notifications off: dropped ${T.dropped} queued push${T.dropped===1?"":"es"}`),h?a(200,await I()):P(t,"/_hub/#notifications")}if(u==="/_hub/settings/keep-awake"&&e.method==="POST"){const r=new URLSearchParams(await E(e));try{vn(r.get("keep_awake"))}catch(o){if(o.status===409)return h?a(409,{error:o.message}):i(409,"text/plain; charset=utf-8",o.message);if(o.status!==400)throw o;return h?a(400,{error:xn}):P(t,"/_hub/?refused=keep-awake#keep-awake")}return re.apply(),h?a(200,await I()):P(t,"/_hub/#keep-awake")}if(u==="/_hub/settings/hub-pause"&&e.method==="POST"){const r=new URLSearchParams(await E(e));try{vs(r.get("pause"))}catch(o){if(o.status!==400)throw o;return h?a(400,{error:o.message}):i(400,"text/plain; charset=utf-8",o.message)}return await he(),h?a(200,await I()):P(t,"/_hub/#pause")}if(u==="/_hub/settings/codex"&&e.method==="POST"){const r=new URLSearchParams(await E(e));try{en(r.get("value"),ne({fresh:!0}))}catch(o){if(o.status!==400&&o.status!==409)throw o;return h?a(o.status,{error:o.message}):i(o.status,"text/plain; charset=utf-8",o.message)}return h?a(200,await ge(e)):P(t,"/_hub/")}if(u==="/_hub/settings/claude-model"&&e.method==="POST"){const r=new URLSearchParams(await E(e));try{nn(r.get("value"))}catch(o){if(o.status!==400)throw o;return h?a(400,{error:o.message}):i(400,"text/plain; charset=utf-8",o.message)}return h?a(200,await ge(e)):P(t,"/_hub/")}if(["/_hub/settings/pause-at","/_hub/settings/keep-going"].includes(u)&&e.method==="POST"){const r=new URLSearchParams(await E(e));try{u.endsWith("/pause-at")?xs(r.get("value")):Ts(r.get("value")),await he()}catch(o){if(o.status!==400&&o.status!==409)throw o;return h?a(o.status,{error:o.message}):i(o.status,"text/plain; charset=utf-8",o.message)}return h?a(200,await ge(e)):P(t,"/_hub/")}if(u==="/_hub/settings/linear"&&e.method==="POST"){const r=new URLSearchParams(await E(e,4096));try{r.has("remove")?(qs(),c("linear: key removed"),await bt()):c(`linear: key ${(await ea(r.get("key"))).refused?"refused by Linear":"saved"}`)}catch(o){if(!(o instanceof Oe))throw o;return h?a(o.status,{error:o.message}):i(o.status,"text/plain; charset=utf-8",o.message)}return h?a(200,await I()):P(t,"/_hub/#linear")}if(u==="/_hub/settings/slack"&&e.method==="POST"){const r=new URLSearchParams(await E(e,4096));try{r.has("remove")?(sa(),c("slack: token removed")):c(`slack: token ${(await aa(r.get("token"))).refused?"refused by Slack":"saved"}`)}catch(o){if(!(o instanceof ta))throw o;return h?a(o.status,{error:o.message}):i(o.status,"text/plain; charset=utf-8",o.message)}return h?a(200,await I()):P(t,"/_hub/#slack")}if(u==="/_hub/api/linear"&&e.method==="GET"){const r=se();if(!r.set||r.refused)return a(200,r);const o=lt();try{const d=await o.teams();return a(200,{...se(),teams:d.map(({key:f,name:T})=>({key:f,name:T})),projects:d.flatMap(f=>f.projects.map(T=>({id:T.id,name:T.name,team:f.key})))})}catch(d){if(!(d instanceof Oe))throw d;return a(200,{...se(),error:d.message})}}if(u==="/_hub/linear/projects"&&e.method==="POST"){const r=lt();if(!r)return a(400,{error:"Add a Linear key on Hub first."});let o;try{o=await v(e,4*1024)}catch(f){return a(f.status||400,{error:"Send the team and project name as JSON."})}const d=typeof o.name=="string"?o.name.trim():"";if(!d||d.length>80||typeof o.team!="string"||!/^[A-Z][A-Z0-9]*$/.test(o.team))return a(400,{error:"Name the project and its team."});try{return a(200,await r.createProject(o.team,d))}catch(f){if(!(f instanceof Oe))throw f;return a(f.status,{error:f.message})}}if(u==="/_hub/settings/quiet"&&e.method==="POST"){const r=new URLSearchParams(await E(e)),o=Object.fromEntries(["start","end"].filter(d=>r.has(d)).map(d=>[d,r.get(d).trim()]));r.get("set")==="hold"&&(o.hold=r.get("value")||"off");try{Xt(gn(ee(),o))}catch(d){if(d.status!==400)throw d;return h?a(400,{error:d.message}):i(400,"text/plain; charset=utf-8",d.message)}return x.refresh(),h?a(200,await I()):P(t,"/_hub/#quiet")}if(u==="/_hub/settings/rules"&&e.method==="POST"){let r,o;try{r=await v(e,64*1024),o=Gn(ke(),r.rules,r.base??[]),nt(o.changes)&&zn(o.rules)}catch(f){if(!(f instanceof Nn)&&!(f instanceof SyntaxError))throw f;return a(400,{error:f.message})}if(!nt(o.changes))return a(200,{rules:o.rules,changed:!1,events:[],failed:[]});const d=await Ye(D(g()),"rules",{scope:"hub",...o.changes});return c(`hub rules saved: rules \u2192 ${d.events.map(f=>f.route).join(", ")||"no board"}${d.failed.length?`; ${d.failed.join("; ")}`:""}`),a(200,{rules:o.rules,changed:!0,...d})}if(u==="/_hub/push/flush"&&e.method==="POST"){const r=await Ge();return r&&c(`push queue: ${r.ok?"sent":"could not send"} ${r.count} queued (from the settings page)`),h?a(200,$n(r)):P(t,"/_hub/")}if(u==="/_hub/poll"&&e.method==="POST"){const r=await v(e);return a(200,await Le({flush:r.flush!==!1}))}if(u==="/_hub/prompts"&&e.method==="POST"){let r;try{r=await v(e,8*1024*1024)}catch(f){if(f.status)throw f;return a(400,{error:"Send the hook input as JSON."})}if(!r?.tool_name||r.hook_event_name&&r.hook_event_name!=="PermissionRequest")return a(400,{error:"Send a PermissionRequest hook input."});const{answer:o,rec:d}=await G(f=>{const T=Ae(),L=T.boards.find(He=>He.owner?.session&&He.owner.session===r.session_id)||null;ye(f,T)&&x.refresh();const q=hn(f,r);if(q)return{answer:q};const be=cn(r,{board:L,live:T.running?.get(r.session_id)});return f.prompts[be.id]=be,{rec:be}});return o?a(200,{state:"answered",answer:o}):(Z(d.route),await xt(r),c(`prompt ${d.id}: ${d.route?`/${d.route}`:d.cwd} ${d.tool}${d.action?` (${d.action})`:""}`),sn(ln(d),{kind:"prompt",immediate:!0}).catch(f=>c(`prompt ${d.id}: push failed: ${f.message}`)),a(200,{id:d.id,deadline:d.deadline}))}if(u==="/_hub/permission-denied"&&e.method==="POST"){let r;try{r=await v(e,8*1024*1024)}catch(o){if(o.status)throw o;return a(400,{error:"Send the hook input as JSON."})}return r?.hook_event_name!=="PermissionDenied"?a(400,{error:"Send a PermissionDenied hook input."}):a(200,{noted:await xt(r)})}const C=/^\/_hub\/prompts\/([\w-]+)(\/wait|\/answer)?$/.exec(u);return C?va(e,t,{id:C[1],op:C[2]?.slice(1)||null,url:n,sendJSON:a}):!1}l(Ta,"hubRoute");async function va(e,t,{id:n,op:s,url:i,sendJSON:a}){if(s==="wait"&&e.method==="GET"){const p=l(()=>G(S=>{const m=R(S.prompts,n);return m?.answer?delete S.prompts[n]:m&&(m.seen=new Date().toISOString()),m}),"seen"),h=l(S=>(Z(S.route),a(200,{state:"answered",answer:S.answer})),"take"),u=await p();if(!u)return a(404,{state:"gone"});if(u.answer)return h(u);const _=Math.max(0,Math.min(Number(i.searchParams.get("ms"))||Je,Je));if(await new Promise(S=>{const m=V.get(n)||new Set;V.set(n,m);const w=l(()=>{clearTimeout(O),m.delete(w),!m.size&&V.get(n)===m&&V.delete(n),S()},"done"),O=setTimeout(w,_);m.add(w),t.on("close",w)}),t.writableEnded||t.destroyed)return;const k=await p();return k?k.answer?h(k):a(200,{state:"pending"}):a(404,{state:"gone"})}if(s==="answer"&&e.method==="POST"){const p=await v(e),h=await G(k=>{const S=R(k.prompts,n);if(!S)return{status:404,error:"That prompt has expired."};if(S.answer)return{status:409,error:"That prompt was already answered."};const{answer:m,error:w}=rn(S,p);if(w)return{status:400,error:w};S.answer=m;const O=Ae();return ye(k,O),dn(k,S,O),{rec:S,answer:m}});if(h.error)return a(h.status,{error:h.error});const{rec:u,answer:_}=h;c(`prompt ${n}: ${_.choice}`);for(const k of[...V.get(n)||[]])k();return Z(u.route),a(200,{ok:!0})}if(s===null&&e.method==="DELETE"){const p=await G(h=>{const u=R(h.prompts,n);return delete h.prompts[n],u});return p&&(Z(p.route),c(`prompt ${n}: its hook gave up`)),a(200,{ok:!0})}return!1}l(va,"promptRoute");const Oa=l(({icon:e="",title:t,sub:n=""})=>`<div class="row">${e}<span class="body"><span class="text"><span class="title">${t}</span>${n?`<span class="sub">${n}</span>`:""}</span></span></div>`,"row");function It(e,t){const n=j.get(e)||{},s=U(t);return s==="port"?"stopped":s==="files"?"missing":n.why==="code"?"code":n.child?"starting":"crashed"}l(It,"downWhy");function Mt(e,t,n){e.writeHead(502,{"content-type":"text/html; charset=utf-8","cache-control":"no-store","x-kickflip-down":It(t,n)}),e.end(ja(t,n))}l(Mt,"sendDown");function ja(e,t){const n=j.get(e)||{},s=n.exitCode!==void 0&&n.exitCode!==null?` (last exit: ${n.exitCode})`:"",i={stopped:`down \xB7 nothing answers on port ${t.port}`,missing:"down \xB7 its folder is missing",code:"restarting \xB7 its code changed",starting:"starting \xB7 waiting for it to answer",crashed:`down \xB7 restarting${s}`},a=It(e,t),p=a==="code"||a==="starting"?"wait":"down",h=U(t),u=h==="cmd"?"The hub runs every site and restarts one that stops.":h==="port"?"Its session runs this server, so it comes back when that session starts it again.":"The hub serves this page from a folder that is no longer there.";return`${At(`${t.title||e} is down`,'<meta http-equiv="refresh" content="5">')}
9
+ <meta name="apple-mobile-web-app-title" content="Claude">${t}<link rel="stylesheet" href="/_hub/colors.css?v=${at()}"><link rel="stylesheet" href="/_hub/hub.css"></head><body><main>`,"head"),xa=l(e=>/\bapplication\/json\b/.test(e.headers.accept||""),"wantsJSON"),Ta=l(()=>({running:!!y.running,nextAt:St(),last:y.last,present:F.state().present,every:Ze({present:F.state().present,remaining:y.remaining}),low:y.remaining!==null&&y.remaining<yn}),"pollState");async function ge(e){await xt();const t=new Date,{data:n,links:s,rows:i,code:a}=await X();return{...kn({now:t,st:ze(t),poll:Ta(),sidebar:n,rows:i,links:s,rules:ke(),integrations:await Rs.rows(),code:a,version:ut,host:e.headers["x-forwarded-host"]||e.headers.host||"",codexShare:Ke(ne()),claudeModel:nn(),codex:ne(),awake:Qe(t),remembered:new Map(D(g()).map(p=>[p.route,Ve(p.route)]))}),prompts:Je(null).map(dn),remembered:Ve(null)}}l(ge,"hubData");const I=l(async()=>{const e=new Date,t={until:Qe(e),unsupported:jn()},n=te({now:e}),s=n.manual==="hub"?Pn(n.resets_at,e):null;return{...$n({now:e,st:ze(e),rules:ke(),linear:se(),slack:sa(),awake:t,pausedUntil:s}),closed:hs()}},"settingsData"),P=l((e,t)=>{e.writeHead(303,{location:t,"cache-control":"no-store"}),e.end()},"redirect");async function va(e,t,n,s=null){const i=l((r,o,d,f={})=>{t.writeHead(r,{"content-type":o,"cache-control":"no-store",...f}),t.end(d)},"send"),a=l((r,o)=>{const d=Hn();return i(r,"application/json",JSON.stringify(o),d?{"x-hub-code":d}:{})},"sendJSON"),p=l(({route:r,started:o})=>(o.then(({error:d})=>{d&&c(`new board /${r} didn't start: ${d}`),x.refresh()}),x.refresh(),a(200,{route:r})),"starting"),h=xa(e),u=n.pathname,_=/^\/_hub\/close-board\/([a-z0-9-]+)$/.exec(u);if(_&&e.method==="GET")try{return a(200,await us(_[1]))}catch(r){return a(r.status||400,{error:r.message})}if(["/_hub/close-board","/_hub/reopen-board"].includes(u)&&e.method==="POST"){let r;try{const o=await v(e);if(r=o.route,u==="/_hub/reopen-board"){const T=await fs(r);return x.refresh(),a(200,T)}if(B.has(r))return a(409,{error:"That board is already closing."});B.add(r);const d=l(()=>B.delete(r),"release");t.once("finish",d),t.once("close",d),await A.running;const f=await ds(r,o.name);return x.refresh(),a(200,f)}catch(o){return a(o.status||400,{error:o.message})}}if(u==="/_hub/restart-board"&&e.method==="POST"){let r;try{if(r=(await v(e)).route,J.has(r))return a(409,{error:"That board is already restarting."});J.add(r),await A.running;const o=await ps(r,{serverUp:ha,portUp:Re,onStarting:l(d=>c(`restart /${r}: ${d}\u2019s session is back, its server is still starting`),"onStarting")});return c(`restarted /${r}: server answers, session ${o.session} resumed`),a(200,o)}catch(o){return r&&c(`restart /${r} failed: ${o.message}`),a(o.status||400,{error:o.message})}finally{J.delete(r),x.refresh()}}if(u==="/_hub/restart-hub"&&e.method==="POST")try{return await v(e),Ee?a(409,{error:"The hub is already restarting."}):(Ee=!0,t.once("finish",()=>{c("restarting the hub from the hub page");const r=ct("hub");r&&(Ee=!1,c(`hub restart failed: ${r}`))}),a(200,{restarting:!0,command:qs().restartCommand}))}catch(r){return a(r.status||400,{error:r.message})}if(u==="/_hub/presence"&&e.method==="POST")return await v(e),kt(F.beat()),t.writeHead(204,{"cache-control":"no-store"}),t.end();if(u==="/_hub/wake"&&e.method==="POST")try{const{route:r}=await v(e),o=D(g()).find(d=>d.route===r);return o?(Ce.add(r),await A.running,_t(),await A.running,Ie.clear(),x.changed(r),a(200,{route:r,listen:Xe(o)})):a(404,{error:"That board is not open."})}catch(r){return a(r.status||400,{error:r.message})}if(u==="/_hub/repos"&&e.method==="GET")try{return a(200,await M.list(g(),{fresh:n.searchParams.get("fresh")==="1"}))}catch(r){return a(r.status||502,{error:r.message})}if(u==="/_hub/branches"&&e.method==="GET")try{return a(200,await M.branches(n.searchParams.get("github")??n.searchParams.get("remote")))}catch(r){return a(r.status||502,{error:r.message})}if(u==="/_hub/repo"&&e.method==="GET")return ts(n,{repos:M,manifest:g(),sendJSON:a});const k=/^\/_hub\/board-repo\/([a-z0-9-]+)$/.exec(u);if(k&&["GET","POST"].includes(e.method))try{const r=e.method==="GET"?st(k[1]):await ns(k[1],await v(e),{repos:M});return e.method==="POST"&&x.refresh(),a(200,r)}catch(r){return a(r.status||400,{error:r.message})}if(u==="/_hub/new-board"&&e.method==="POST"){if(!$e(e.headers))return i(403,"text/plain; charset=utf-8","Send name, github, branch and purpose as application/json.");let r;try{r=await v(e)}catch{return i(400,"text/plain; charset=utf-8","Send name, github, branch and purpose as JSON.")}try{return p(await rs(r,{repos:M,changed:l(()=>x.refresh(),"changed")}))}catch(o){if(o.status)return i(o.status,"text/plain; charset=utf-8",o.message);throw o}}const S=/^\/_hub\/new-board\/([a-z0-9-]+)\/retry$/.exec(u);if(S&&e.method==="POST"){if(!$e(e.headers))return i(403,"text/plain; charset=utf-8","Send the retry as application/json.");try{return p(await os(S[1],{repos:M,changed:l(()=>x.refresh(),"changed")}))}catch(r){if(r.status)return i(r.status,"text/plain; charset=utf-8",r.message);throw r}}const m=/^\/_hub\/new-board\/([a-z0-9-]+)$/.exec(u);if(m&&e.method==="DELETE")try{const r=await is(m[1]);return x.refresh(),a(200,r)}catch(r){if(r.status)return i(r.status,"text/plain; charset=utf-8",r.message);throw r}if((u==="/_hub/build"||u==="/_hub/pull")&&e.method==="POST"){if(ie)return a(503,{error:"The hub is stopping. Try again after it restarts."});const r=["127.0.0.1","::1","::ffff:127.0.0.1"],o=(e.headers.host||"").replace(/:\d+$/,"");if(!r.includes(e.socket.remoteAddress)||!["127.0.0.1","localhost","[::1]"].includes(o)||e.headers.origin||Object.keys(e.headers).some(f=>f==="forwarded"||f.startsWith("x-forwarded-")))return a(403,{error:`${u==="/_hub/pull"?"Pulls":"Builds"} can only be started from the host over loopback.`});let d;try{d=await v(e)}catch(f){return a(f.status||400,{error:u==="/_hub/pull"?"Send the board route as JSON.":"Send the board route and card key as JSON."})}try{return u==="/_hub/pull"?a(200,await Is(d,{manifest:g()})):a(202,await Ds(d,{manifest:g(),log:c}))}catch(f){if(f instanceof Es)return a(f.status,{error:f.message});throw f}}if(u==="/sw.js")return i(200,"text/javascript; charset=utf-8",b.readFileSync($.join(z,"sw.js")),{"service-worker-allowed":"/"});if(u==="/manifest.webmanifest")return i(200,"application/manifest+json",_a());const w=/^\/_hub\/icon-(180|192|512)\.png$/.exec(u);if(w)try{return i(200,"image/png",b.readFileSync($.join(z,`icon-${w[1]}.png`)),{"cache-control":"max-age=86400"})}catch{t.writeHead(404).end();return}if(u==="/_hub/colors.css"){const r=n.searchParams.get("v")===at()?"public, max-age=31536000, immutable":"no-cache";try{return i(200,"text/css; charset=utf-8",b.readFileSync(ia),{"cache-control":r})}catch{t.writeHead(404).end();return}}if(u==="/_hub/kickflip.png")try{return i(200,"image/png",b.readFileSync($.join(_e,"kickflip.png")),{"cache-control":"max-age=86400"})}catch{t.writeHead(404).end();return}if(u==="/_hub/hub.css")return i(200,"text/css; charset=utf-8",b.readFileSync($.join(_e,"hub.css")));if(u==="/_hub/sidebar.json"){const{data:r}=await X({fresh:n.searchParams.get("fresh")==="1"});return a(200,s?s.sidebar(r):r)}if(u==="/_hub/stream"&&e.method==="GET"){if(s){const r={login:s.login,routes:s.routes,res:t};we.add(r),e.on("close",()=>we.delete(r)),e.on("close",Y.watching(s.login))}x.handle(e,t,s&&{routes:s.routes,view:s.sidebar});return}const O=/^\/_hub\/share\/([a-z0-9-]+)$/.exec(u);if(O&&e.method==="GET"){const r=R(g().sites,O[1]);if(!Se(r))return a(404,{error:"That board is not open."});const o=Vs(et($.join(r.dir,"board.json"),{})),d=o.map(L=>L.login),f=(await Te.node())?.dnsName||e.headers["x-forwarded-host"],T=Y.people(O[1],d).map((L,q)=>({...L,level:o[q].level}));return a(200,{people:T,refused:Y.refusals(O[1],d),link:f?`https://${f}/${O[1]}/`:null})}if(u.startsWith("/_hub/app/")){const r=ws(u.slice(10));if(!r){t.writeHead(404).end();return}const o=$.extname(r),d={".js":"text/javascript; charset=utf-8",".css":"text/css; charset=utf-8",".png":"image/png"}[o],f=n.searchParams.get("v")===ys(r)?"public, max-age=31536000, immutable":"no-cache";return o===".png"?i(200,d,b.readFileSync(r),{"cache-control":f}):Ct(e,t,200,{"content-type":d,"cache-control":f},b.readFileSync(r))}if(u==="/_hub/api/hub")return a(200,await ge(e));if(u==="/_hub/api/settings")return a(200,await I());if(u==="/_hub/push/key")return a(200,{key:Jt()});if(u==="/_hub/push/subscribe"&&e.method==="POST"){const r=e.headers["x-forwarded-host"]||e.headers.host;return a(200,Vt(await v(e),`https://${r}/`,e.headers["user-agent"]))}if(u==="/_hub/push/forget"&&e.method==="POST")return a(200,{count:Qt((await v(e))?.id)});if(u==="/_hub/push/unsubscribe"&&e.method==="POST")return a(200,{count:Zt((await v(e))?.endpoint)});if(u==="/_hub/push/test"&&e.method==="POST"){const r=await Xt({title:"Claude",body:"Test notification from the hub.",url:"/",tag:"hub-test"});return a(200,{sent:r.filter(o=>o.status>=200&&o.status<300).length,results:r})}if(u==="/_hub/settings/notifications"&&e.method==="POST"){const r=new URLSearchParams(await E(e)),o=r.get("set"),d=o==="on"?{on:r.get("value")==="on"}:o==="weekends"?{weekends_off:r.get("value")==="on"}:r.has("snooze")?{snooze:r.get("snooze")}:null,f="Pass set=on or set=weekends with value=on, or snooze.";if(!d)return h?a(400,{error:f}):i(400,"text/plain; charset=utf-8",f);let T;try{T=await en(d)}catch(L){if(L.status!==400)throw L;return h?a(400,{error:xn}):P(t,"/_hub/?refused=snooze#notifications")}return T.dropped&&c(`notifications off: dropped ${T.dropped} queued push${T.dropped===1?"":"es"}`),h?a(200,await I()):P(t,"/_hub/#notifications")}if(u==="/_hub/settings/keep-awake"&&e.method==="POST"){const r=new URLSearchParams(await E(e));try{On(r.get("keep_awake"))}catch(o){if(o.status===409)return h?a(409,{error:o.message}):i(409,"text/plain; charset=utf-8",o.message);if(o.status!==400)throw o;return h?a(400,{error:Tn}):P(t,"/_hub/?refused=keep-awake#keep-awake")}return re.apply(),h?a(200,await I()):P(t,"/_hub/#keep-awake")}if(u==="/_hub/settings/hub-pause"&&e.method==="POST"){const r=new URLSearchParams(await E(e));try{Os(r.get("pause"))}catch(o){if(o.status!==400)throw o;return h?a(400,{error:o.message}):i(400,"text/plain; charset=utf-8",o.message)}return await he(),h?a(200,await I()):P(t,"/_hub/#pause")}if(u==="/_hub/settings/codex"&&e.method==="POST"){const r=new URLSearchParams(await E(e));try{tn(r.get("value"),ne({fresh:!0}))}catch(o){if(o.status!==400&&o.status!==409)throw o;return h?a(o.status,{error:o.message}):i(o.status,"text/plain; charset=utf-8",o.message)}return h?a(200,await ge(e)):P(t,"/_hub/")}if(u==="/_hub/settings/claude-model"&&e.method==="POST"){const r=new URLSearchParams(await E(e));try{sn(r.get("value"))}catch(o){if(o.status!==400)throw o;return h?a(400,{error:o.message}):i(400,"text/plain; charset=utf-8",o.message)}return h?a(200,await ge(e)):P(t,"/_hub/")}if(["/_hub/settings/pause-at","/_hub/settings/keep-going"].includes(u)&&e.method==="POST"){const r=new URLSearchParams(await E(e));try{u.endsWith("/pause-at")?Ts(r.get("value")):vs(r.get("value")),await he()}catch(o){if(o.status!==400&&o.status!==409)throw o;return h?a(o.status,{error:o.message}):i(o.status,"text/plain; charset=utf-8",o.message)}return h?a(200,await ge(e)):P(t,"/_hub/")}if(u==="/_hub/settings/linear"&&e.method==="POST"){const r=new URLSearchParams(await E(e,4096));try{r.has("remove")?(ea(),c("linear: key removed"),await yt()):c(`linear: key ${(await ta(r.get("key"))).refused?"refused by Linear":"saved"}`)}catch(o){if(!(o instanceof Oe))throw o;return h?a(o.status,{error:o.message}):i(o.status,"text/plain; charset=utf-8",o.message)}return h?a(200,await I()):P(t,"/_hub/#linear")}if(u==="/_hub/settings/slack"&&e.method==="POST"){const r=new URLSearchParams(await E(e,4096));try{r.has("remove")?(aa(),c("slack: token removed")):c(`slack: token ${(await ra(r.get("token"))).refused?"refused by Slack":"saved"}`)}catch(o){if(!(o instanceof na))throw o;return h?a(o.status,{error:o.message}):i(o.status,"text/plain; charset=utf-8",o.message)}return h?a(200,await I()):P(t,"/_hub/#slack")}if(u==="/_hub/api/linear"&&e.method==="GET"){const r=se();if(!r.set||r.refused)return a(200,r);const o=lt();try{const d=await o.teams();return a(200,{...se(),teams:d.map(({key:f,name:T})=>({key:f,name:T})),projects:d.flatMap(f=>f.projects.map(T=>({id:T.id,name:T.name,team:f.key})))})}catch(d){if(!(d instanceof Oe))throw d;return a(200,{...se(),error:d.message})}}if(u==="/_hub/linear/projects"&&e.method==="POST"){const r=lt();if(!r)return a(400,{error:"Add a Linear key on Hub first."});let o;try{o=await v(e,4*1024)}catch(f){return a(f.status||400,{error:"Send the team and project name as JSON."})}const d=typeof o.name=="string"?o.name.trim():"";if(!d||d.length>80||typeof o.team!="string"||!/^[A-Z][A-Z0-9]*$/.test(o.team))return a(400,{error:"Name the project and its team."});try{return a(200,await r.createProject(o.team,d))}catch(f){if(!(f instanceof Oe))throw f;return a(f.status,{error:f.message})}}if(u==="/_hub/settings/quiet"&&e.method==="POST"){const r=new URLSearchParams(await E(e)),o=Object.fromEntries(["start","end"].filter(d=>r.has(d)).map(d=>[d,r.get(d).trim()]));r.get("set")==="hold"&&(o.hold=r.get("value")||"off");try{Yt(wn(ee(),o))}catch(d){if(d.status!==400)throw d;return h?a(400,{error:d.message}):i(400,"text/plain; charset=utf-8",d.message)}return x.refresh(),h?a(200,await I()):P(t,"/_hub/#quiet")}if(u==="/_hub/settings/rules"&&e.method==="POST"){let r,o;try{r=await v(e,64*1024),o=zn(ke(),r.rules,r.base??[]),nt(o.changes)&&Kn(o.rules)}catch(f){if(!(f instanceof Gn)&&!(f instanceof SyntaxError))throw f;return a(400,{error:f.message})}if(!nt(o.changes))return a(200,{rules:o.rules,changed:!1,events:[],failed:[]});const d=await Ye(D(g()),"rules",{scope:"hub",...o.changes});return c(`hub rules saved: rules \u2192 ${d.events.map(f=>f.route).join(", ")||"no board"}${d.failed.length?`; ${d.failed.join("; ")}`:""}`),a(200,{rules:o.rules,changed:!0,...d})}if(u==="/_hub/push/flush"&&e.method==="POST"){const r=await Ge();return r&&c(`push queue: ${r.ok?"sent":"could not send"} ${r.count} queued (from the settings page)`),h?a(200,_n(r)):P(t,"/_hub/")}if(u==="/_hub/poll"&&e.method==="POST"){const r=await v(e);return a(200,await Le({flush:r.flush!==!1}))}if(u==="/_hub/prompts"&&e.method==="POST"){let r;try{r=await v(e,8*1024*1024)}catch(f){if(f.status)throw f;return a(400,{error:"Send the hook input as JSON."})}if(!r?.tool_name||r.hook_event_name&&r.hook_event_name!=="PermissionRequest")return a(400,{error:"Send a PermissionRequest hook input."});const{answer:o,rec:d}=await G(f=>{const T=Ae(),L=T.boards.find(He=>He.owner?.session&&He.owner.session===r.session_id)||null;ye(f,T)&&x.refresh();const q=fn(f,r);if(q)return{answer:q};const be=ln(r,{board:L,live:T.running?.get(r.session_id)});return f.prompts[be.id]=be,{rec:be}});return o?a(200,{state:"answered",answer:o}):(Z(d.route),await Tt(r),c(`prompt ${d.id}: ${d.route?`/${d.route}`:d.cwd} ${d.tool}${d.action?` (${d.action})`:""}`),an(un(d),{kind:"prompt",immediate:!0}).catch(f=>c(`prompt ${d.id}: push failed: ${f.message}`)),a(200,{id:d.id,deadline:d.deadline}))}if(u==="/_hub/permission-denied"&&e.method==="POST"){let r;try{r=await v(e,8*1024*1024)}catch(o){if(o.status)throw o;return a(400,{error:"Send the hook input as JSON."})}return r?.hook_event_name!=="PermissionDenied"?a(400,{error:"Send a PermissionDenied hook input."}):a(200,{noted:await Tt(r)})}const C=/^\/_hub\/prompts\/([\w-]+)(\/wait|\/answer)?$/.exec(u);return C?Oa(e,t,{id:C[1],op:C[2]?.slice(1)||null,url:n,sendJSON:a}):!1}l(va,"hubRoute");async function Oa(e,t,{id:n,op:s,url:i,sendJSON:a}){if(s==="wait"&&e.method==="GET"){const p=l(()=>G(S=>{const m=R(S.prompts,n);return m?.answer?delete S.prompts[n]:m&&(m.seen=new Date().toISOString()),m}),"seen"),h=l(S=>(Z(S.route),a(200,{state:"answered",answer:S.answer})),"take"),u=await p();if(!u)return a(404,{state:"gone"});if(u.answer)return h(u);const _=Math.max(0,Math.min(Number(i.searchParams.get("ms"))||We,We));if(await new Promise(S=>{const m=V.get(n)||new Set;V.set(n,m);const w=l(()=>{clearTimeout(O),m.delete(w),!m.size&&V.get(n)===m&&V.delete(n),S()},"done"),O=setTimeout(w,_);m.add(w),t.on("close",w)}),t.writableEnded||t.destroyed)return;const k=await p();return k?k.answer?h(k):a(200,{state:"pending"}):a(404,{state:"gone"})}if(s==="answer"&&e.method==="POST"){const p=await v(e),h=await G(k=>{const S=R(k.prompts,n);if(!S)return{status:404,error:"That prompt has expired."};if(S.answer)return{status:409,error:"That prompt was already answered."};const{answer:m,error:w}=on(S,p);if(w)return{status:400,error:w};S.answer=m;const O=Ae();return ye(k,O),hn(k,S,O),{rec:S,answer:m}});if(h.error)return a(h.status,{error:h.error});const{rec:u,answer:_}=h;c(`prompt ${n}: ${_.choice}`);for(const k of[...V.get(n)||[]])k();return Z(u.route),a(200,{ok:!0})}if(s===null&&e.method==="DELETE"){const p=await G(h=>{const u=R(h.prompts,n);return delete h.prompts[n],u});return p&&(Z(p.route),c(`prompt ${n}: its hook gave up`)),a(200,{ok:!0})}return!1}l(Oa,"promptRoute");const ja=l(({icon:e="",title:t,sub:n=""})=>`<div class="row">${e}<span class="body"><span class="text"><span class="title">${t}</span>${n?`<span class="sub">${n}</span>`:""}</span></span></div>`,"row");function Mt(e,t){const n=j.get(e)||{},s=U(t);return s==="port"?"stopped":s==="files"?"missing":n.why==="code"?"code":n.child?"starting":"crashed"}l(Mt,"downWhy");function Ht(e,t,n){e.writeHead(502,{"content-type":"text/html; charset=utf-8","cache-control":"no-store","x-kickflip-down":Mt(t,n)}),e.end(Pa(t,n))}l(Ht,"sendDown");function Pa(e,t){const n=j.get(e)||{},s=n.exitCode!==void 0&&n.exitCode!==null?` (last exit: ${n.exitCode})`:"",i={stopped:`down \xB7 nothing answers on port ${t.port}`,missing:"down \xB7 its folder is missing",code:"restarting \xB7 its code changed",starting:"starting \xB7 waiting for it to answer",crashed:`down \xB7 restarting${s}`},a=Mt(e,t),p=a==="code"||a==="starting"?"wait":"down",h=U(t),u=h==="cmd"?"The hub runs every site and restarts one that stops.":h==="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`${It(`${t.title||e} is down`,'<meta http-equiv="refresh" content="5">')}
10
10
  <a class="back" href="/_hub/">\u2039 Hub</a>
11
11
  <h1>${N(t.title||e)} isn't answering</h1>
12
12
  <p class="lede">${N(u)}</p>
13
13
  <h2 class="label">Site</h2>
14
- <div class="group">${Oa({icon:Wn(e,t),title:N(t.title||e),sub:`<span class="st ${p}">${N(i[a])}</span>`})}</div>
14
+ <div class="group">${ja({icon:Wn(e,t),title:N(t.title||e),sub:`<span class="st ${p}">${N(i[a])}</span>`})}</div>
15
15
  <p class="note">This page retries every 5 seconds.</p>
16
- </main></body></html>`}l(ja,"downPage");const Me=l(e=>new URL(e.url.replace(/^\/+/,"/"),"http://hub"),"requestURL"),Y=Xs({file:$.join(W,"share-seen.json")}),we=new Set;function Ht(){const e=g().sites;for(const t of we){const n=it(t.login,e);[...t.routes].some(([s,i])=>n.get(s)!==i)&&(we.delete(t),t.res.destroy())}Ls()}l(Ht,"endUnshared"),setInterval(Ht,2e3).unref();async function Ut(e){const t=await Te(e),n=e.headers["tailscale-user-login"];if(t!=="other"||!n)return{verdict:t};const s=e.headers["tailscale-user-name"]||null,{pathname:i}=Me(e),a=it(n,g().sites),p=a.size?Vs({method:e.method,pathname:i,routes:a}):"other",h=Zs(i);if(p==="other")return(!a.size||h)&&Y.refuse(n,s,h),{verdict:"other"};if(p!=="pass")return{verdict:p};Y.visit(n,s,h);const u=await Te.node();return{verdict:"shared",viewer:{login:n,routes:a,sidebar:l(_=>Qs(_,a,u?.name||u?.login),"sidebar")}}}l(Ut,"access");const Bt=Ue.createServer((e,t)=>{Ut(e).then(({verdict:n,viewer:s})=>{n==="owner"||n==="shared"?Pa(e,t,s):n==="home"?(t.writeHead(302,{location:"/","cache-control":"no-store"}).end(),e.resume()):Ws(e,t,n)})});function Pa(e,t,n=null){const s=Me(e);if(e.method!=="GET"&&e.method!=="HEAD"&&ms(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 i=new Set(Object.keys(g().sites));if(rt({method:e.method,pathname:s.pathname,search:s.search,dest:ws(e.headers),sites:i})==="shell"){const m=s.pathname==="/"?"":s.pathname.startsWith("/_hub/")?s.pathname.slice(1):decodeURIComponent(s.pathname.split("/")[1]);Sa(m,n).then(w=>Lt(e,t,200,{"content-type":"text/html; charset=utf-8","cache-control":"no-cache",vary:"sec-fetch-dest","x-app-shell":"1"},w)).catch(w=>{t.headersSent||t.writeHead(500,{"content-type":"text/plain"}),t.end(String(w.message||w))});return}if(s.pathname==="/_hub"){t.writeHead(302,{location:"/_hub/"+s.search}).end();return}if(s.pathname==="/_hub/status.json"){t.writeHead(200,{"content-type":"application/json","cache-control":"no-store"}),t.end(JSON.stringify({hub:{port:je,pid:process.pid},sites:fe()},null,2));return}if(s.pathname==="/_hub/needs.json"){const m=fe();Et(m,{fresh:s.searchParams.get("fresh")==="1"}).then(w=>{t.writeHead(200,{"content-type":"application/json","cache-control":"no-store"}),t.end(JSON.stringify({total:Dt(w),sites:Object.fromEntries(m.map(O=>[O.route,w.get(O.route)??null]))}))}).catch(w=>{t.headersSent||t.writeHead(500,{"content-type":"text/plain"}),t.end(String(w.message||w))});return}if(s.pathname==="/favicon.ico"){t.writeHead(301,{location:"/_hub/kickflip.png"}).end();return}if(s.pathname==="/sw.js"||s.pathname==="/manifest.webmanifest"||s.pathname.startsWith("/_hub/")){Ta(e,t,s,n).then(m=>{m===!1&&t.writeHead(404).end()}).catch(m=>{t.headersSent||t.writeHead(m.status||500,{"content-type":"text/plain"}),t.end(String(m.message||m))});return}const a=s.pathname.split("/")[1],p=R(g().sites,a);if(!p){t.writeHead(404,{"content-type":"text/html; charset=utf-8","x-kickflip-down":"removed"}),t.end(`${At("No Site")}<a class="back" href="/_hub/">\u2039 Hub</a><h1>No site at /${N(a)}</h1></main></body></html>`);return}if(s.pathname==="/"+a){t.writeHead(302,{location:"/"+a+"/"+s.search}).end();return}const h="/"+a,u=n?s.pathname.slice(h.length)+s.search:e.url.slice(h.length)||"/",_=rt({pathname:s.pathname,dest:"document",sites:i})==="shell";if(U(p)==="files"){if(!Pt(p.dir)){Mt(t,a,p);return}Ms(e,t,{dir:p.dir,rest:u,prefix:h,headers:_?{vary:"sec-fetch-dest"}:{}}).catch(m=>{t.headersSent||t.writeHead(500,{"content-type":"text/plain"}),t.end(String(m.message||m))});return}const k={...e.headers,"x-forwarded-prefix":h,"x-forwarded-host":e.headers["x-forwarded-host"]||e.headers.host,"x-forwarded-proto":e.headers["x-forwarded-proto"]||"https"};delete k["x-kickflip-shared"],n&&(k["x-kickflip-shared"]=n.login);const S=Ue.request({host:"127.0.0.1",port:p.port,method:e.method,path:u,headers:k},m=>{const w={...m.headers,"x-kickflip-hub":"1"};_&&(w.vary=[w.vary,"sec-fetch-dest"].filter(Boolean).join(", ")),typeof w.location=="string"&&w.location.startsWith("/")&&!w.location.startsWith("//")&&!w.location.startsWith(h+"/")&&w.location!==h&&(w.location=h+w.location),t.writeHead(m.statusCode,w),m.pipe(t),m.on("close",()=>{m.complete||t.destroy()})});t.on("close",()=>{t.writableFinished||S.destroy()}),!n&&e.method==="POST"&&u.split("?")[0]==="/api/share"&&t.on("finish",Ht),S.on("error",()=>{if(t.headersSent){t.destroy();return}Mt(t,a,p)}),e.pipe(S)}l(Pa,"serve"),Bt.on("upgrade",async(e,t,n)=>{const{verdict:s}=await Ut(e);if(s!=="owner"){Js(t,s==="shared"?"view-only":s==="home"?"other":s);return}const i=Me(e).pathname.split("/")[1],a=R(g().sites,i);if(!a?.port){t.destroy();return}const p=`/${i}`;zs({req:e,socket:t,head:n,port:a.port,path:e.url.slice(p.length)||"/",replace:["x-forwarded-prefix"],extra:[`X-Forwarded-Prefix: ${p}`]})}),Bt.listen(je,"127.0.0.1",()=>c(`hub on http://127.0.0.1:${je}/ (manifest ${dt})`));
16
+ </main></body></html>`}l(Pa,"downPage");const Me=l(e=>new URL(e.url.replace(/^\/+/,"/"),"http://hub"),"requestURL"),Y=Ys({file:$.join(K,"share-seen.json")}),we=new Set;function Ut(){const e=g().sites;for(const t of we){const n=it(t.login,e);[...t.routes].some(([s,i])=>n.get(s)!==i)&&(we.delete(t),t.res.destroy())}Cs()}l(Ut,"endUnshared"),setInterval(Ut,2e3).unref();async function Bt(e){const t=await Te(e),n=e.headers["tailscale-user-login"];if(t!=="other"||!n)return{verdict:t};const s=e.headers["tailscale-user-name"]||null,{pathname:i}=Me(e),a=it(n,g().sites),p=a.size?Zs({method:e.method,pathname:i,routes:a}):"other",h=Qs(i);if(p==="other")return(!a.size||h)&&Y.refuse(n,s,h),{verdict:"other"};if(p!=="pass")return{verdict:p};Y.visit(n,s,h);const u=await Te.node();return{verdict:"shared",viewer:{login:n,routes:a,sidebar:l(_=>Xs(_,a,u?.name||u?.login),"sidebar")}}}l(Bt,"access");const Ft=Ue.createServer((e,t)=>{Bt(e).then(({verdict:n,viewer:s})=>{n==="owner"||n==="shared"?Ra(e,t,s):n==="home"?(t.writeHead(302,{location:"/","cache-control":"no-store"}).end(),e.resume()):Ws(e,t,n)})});function Ra(e,t,n=null){const s=Me(e);if(e.method!=="GET"&&e.method!=="HEAD"&&gs(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 i=new Set(Object.keys(g().sites));if(rt({method:e.method,pathname:s.pathname,search:s.search,dest:bs(e.headers),sites:i})==="shell"){const m=s.pathname==="/"?"":s.pathname.startsWith("/_hub/")?s.pathname.slice(1):decodeURIComponent(s.pathname.split("/")[1]);ka(m,n).then(w=>Ct(e,t,200,{"content-type":"text/html; charset=utf-8","cache-control":"no-cache",vary:"sec-fetch-dest","x-app-shell":"1"},w)).catch(w=>{t.headersSent||t.writeHead(500,{"content-type":"text/plain"}),t.end(String(w.message||w))});return}if(s.pathname==="/_hub"){t.writeHead(302,{location:"/_hub/"+s.search}).end();return}if(s.pathname==="/_hub/status.json"){t.writeHead(200,{"content-type":"application/json","cache-control":"no-store"}),t.end(JSON.stringify({hub:{port:je,pid:process.pid},sites:fe()},null,2));return}if(s.pathname==="/_hub/needs.json"){const m=fe();Dt(m,{fresh:s.searchParams.get("fresh")==="1"}).then(w=>{t.writeHead(200,{"content-type":"application/json","cache-control":"no-store"}),t.end(JSON.stringify({total:Lt(w),sites:Object.fromEntries(m.map(O=>[O.route,w.get(O.route)??null]))}))}).catch(w=>{t.headersSent||t.writeHead(500,{"content-type":"text/plain"}),t.end(String(w.message||w))});return}if(s.pathname==="/favicon.ico"){t.writeHead(301,{location:"/_hub/kickflip.png"}).end();return}if(s.pathname==="/sw.js"||s.pathname==="/manifest.webmanifest"||s.pathname.startsWith("/_hub/")){va(e,t,s,n).then(m=>{m===!1&&t.writeHead(404).end()}).catch(m=>{t.headersSent||t.writeHead(m.status||500,{"content-type":"text/plain"}),t.end(String(m.message||m))});return}const a=s.pathname.split("/")[1],p=R(g().sites,a);if(!p){t.writeHead(404,{"content-type":"text/html; charset=utf-8","x-kickflip-down":"removed"}),t.end(`${It("No Site")}<a class="back" href="/_hub/">\u2039 Hub</a><h1>No site at /${N(a)}</h1></main></body></html>`);return}if(s.pathname==="/"+a){t.writeHead(302,{location:"/"+a+"/"+s.search}).end();return}const h="/"+a,u=n?s.pathname.slice(h.length)+s.search:e.url.slice(h.length)||"/",_=rt({pathname:s.pathname,dest:"document",sites:i})==="shell";if(U(p)==="files"){if(!Rt(p.dir)){Ht(t,a,p);return}Hs(e,t,{dir:p.dir,rest:u,prefix:h,headers:_?{vary:"sec-fetch-dest"}:{}}).catch(m=>{t.headersSent||t.writeHead(500,{"content-type":"text/plain"}),t.end(String(m.message||m))});return}const k={...e.headers,"x-forwarded-prefix":h,"x-forwarded-host":e.headers["x-forwarded-host"]||e.headers.host,"x-forwarded-proto":e.headers["x-forwarded-proto"]||"https"};delete k["x-kickflip-shared"],n&&(k["x-kickflip-shared"]=n.login);const S=Ue.request({host:"127.0.0.1",port:p.port,method:e.method,path:u,headers:k},m=>{const w={...m.headers,"x-kickflip-hub":"1"};_&&(w.vary=[w.vary,"sec-fetch-dest"].filter(Boolean).join(", ")),typeof w.location=="string"&&w.location.startsWith("/")&&!w.location.startsWith("//")&&!w.location.startsWith(h+"/")&&w.location!==h&&(w.location=h+w.location),t.writeHead(m.statusCode,w),m.pipe(t),m.on("close",()=>{m.complete||t.destroy()})});t.on("close",()=>{t.writableFinished||S.destroy()}),!n&&e.method==="POST"&&u.split("?")[0]==="/api/share"&&t.on("finish",Ut),S.on("error",()=>{if(t.headersSent){t.destroy();return}Ht(t,a,p)}),e.pipe(S)}l(Ra,"serve"),Ft.on("upgrade",async(e,t,n)=>{const{verdict:s}=await Bt(e);if(s!=="owner"){Js(t,s==="shared"?"view-only":s==="home"?"other":s);return}const i=Me(e).pathname.split("/")[1],a=R(g().sites,i);if(!a?.port){t.destroy();return}const p=`/${i}`;Ks({req:e,socket:t,head:n,port:a.port,path:e.url.slice(p.length)||"/",replace:["x-forwarded-prefix"],extra:[`X-Forwarded-Prefix: ${p}`]})}),Ft.listen(je,"127.0.0.1",()=>c(`hub on http://127.0.0.1:${je}/ (manifest ${dt})`));
package/hub/sessions.mjs CHANGED
@@ -1,2 +1,2 @@
1
- var ce=Object.defineProperty;var a=(e,s)=>ce(e,"name",{value:s,configurable:!0});import O from"node:fs";import g from"node:path";import A from"node:os";import{spawn as J,spawnSync as de}from"node:child_process";import{fileURLToPath as ue}from"node:url";import{joinName as le,routeRecord as h,validRoute as fe}from"./routes.mjs";import{withFileLock as pe}from"./locks.mjs";import{RESUME_STATE as P,carryCursor as me,hubBoard as H,isListening as he,listBoards as U,listenState as we,oldestUnheard as ge,readManifest as _,readJSON as N,saveJSON as X,watchedCounts as be,watchedLines as $e,watcherCursor as ke,withManifestLock as ye,writeManifest as Ie}from"./boards.mjs";import{readUsageGate as Se,heldUsageEvent as _e}from"../shared/usage-gate.mjs";import{liveSessions as x,sessionRecords as Te,processStarts as xe,verifiedProcess as je}from"./live.mjs";import{deliver as ve}from"./push.mjs";import{WAIT_MS as Oe}from"./prompts.mjs";import{nodeBin as q,storedPath as Q}from"../bin/install.mjs";import{reposDir as Ae}from"../shared/repos.mjs";import{isClone as Ee}from"./repos.mjs";import{trustFolder as Ce}from"./trust.mjs";import{contextFull as De,contextReading as Le}from"./usage.mjs";import{audit as Y,boardAudit as z}from"../shared/audit.mjs";const E=Q(g.resolve(g.dirname(ue(import.meta.url)),"..")),Pe=3*6e4,Ue=5*6e4,Ne=15*6e4,V=g.join(A.homedir(),".claude","kickflip","compact-state.json"),Be=a((e,s=Date.now())=>!!e.error||s-Date.parse(e.asked_at||e.since)>=Ne,"startFailed");function Me(e){return e.joined_at?{step:"landing",stepAt:e.joined_at}:e.phase==="cloning"?{step:"cloning",stepAt:e.asked_at}:{step:"starting",stepAt:e.phase_at||e.asked_at||e.since}}a(Me,"startStep");function $s(e,s=Date.now(),t=()=>{},r=()=>!0){return Object.entries(e.starting||{}).filter(([i,n])=>h(e.sites,i)?n.joined_at&&!r(i):!0).map(([i,n])=>{const o=Be(n,s);return{route:i,title:n.title||n.label||i,repo:n.repo,...Me(n),...n.phase==="cloning"&&t(i)!==void 0?{percent:t(i)}:{},...n.branch?{branch:n.branch,tickets:n.tickets}:{},askedAt:n.asked_at||n.since,failed:o,...n.joined_at?{joined:!0}:{},...n.error?{error:n.error}:o&&n.joined_at?{error:"Its session joined, but the board is not listening yet."}:{}}})}a($s,"startingBoards");const Re=Oe/1e3+60,Fe=JSON.stringify({worktree:{bgIsolation:"none"},permissions:{deny:["Workflow"]},hooks:{PermissionRequest:[{matcher:"*",hooks:[{type:"command",command:`"${q()}" "${g.join(E,"hub","permission-hook.mjs")}"`,timeout:Re}]}],PermissionDenied:[{matcher:"Bash",hooks:[{type:"command",command:`"${q()}" "${g.join(E,"hub","permission-denied-hook.mjs")}"`,timeout:15}]}]}});class m extends Error{static{a(this,"SessionError")}constructor(s,t){super(s),this.reason=t}}function Ge(e,s){if(!fe(e))throw new m("Choose a board route.","none");return pe(g.join(A.homedir(),".claude","kickflip","session-locks",e),()=>s({resume:a(t=>Ve(e,t),"resume")}),{staleAfter:12e4,timeout:9e4})}a(Ge,"withSessionLock");function C(){if(process.env.KICKFLIP_CLAUDE)return process.env.KICKFLIP_CLAUDE;const e=g.join(A.homedir(),".local","bin","claude");return O.existsSync(e)?e:"claude"}a(C,"claudeBin");const D=a(e=>e==="CLAUDECODE"||e==="CLAUDE_PID"||e==="CLAUDE_EFFORT"||e.startsWith("CLAUDE_CODE_"),"inherited");function Z(){return process.env.KICKFLIP_TMUX?process.env.KICKFLIP_TMUX:["/opt/homebrew/bin/tmux","/usr/local/bin/tmux","/usr/bin/tmux"].find(e=>O.existsSync(e))||"tmux"}a(Z,"tmuxBin");function B(e){const s=de(Z(),e,{encoding:"utf8",timeout:1e4,env:Object.fromEntries(Object.entries(process.env).filter(([t])=>!D(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(B,"runTmux");const T=a(e=>`kickflip-${e}`,"tmuxName"),M=a((e,s)=>e(["has-session","-t",`=${T(s)}`]).status===0,"hasTmux");async function ks({manifest:e=_(),now:s=new Date,running:t=x(),wakes:r=N(P,{}),tmux:i=B}={}){if(!t)return{compacted:[],failed:[]};const n=Object.assign(Object.create(null),N(V,{})),o=[],f=[];let d=!1;const c=H(e)?.route;for(const p of U(e)){const l=Le(p.owner?.session);if(!De(we(p,{now:s.getTime(),running:t,wakes:r}),l)){h(n,p.route)&&(delete n[p.route],d=!0);continue}if(p.route!==c||h(n,p.route)||!M(i,p.route))continue;const u=`=${T(p.route)}:`,y=i(["send-keys","-t",u,"-l","/compact"]),b=y.status===0?i(["send-keys","-t",u,"Enter"]):y,w=b.status===0?null:(b.stderr||b.stdout).trim().slice(0,300)||`tmux exited ${b.status}`;n[p.route]={session:p.owner.session,percent:l.percent,at:s.toISOString(),...w?{error:w}:{}},Y(z(p.dir),{call:"compact",result:w?"failed":"typed /compact",reason:`Its session's context is ${l.percent}% full and it isn't listening.${w?` ${w}`:""}`,inputs:{session:p.owner.session,context:l}},{now:s}),d=!0,w?f.push(`${p.route}: could not send /compact to tmux session ${T(p.route)} (${w})`):o.push(p.route)}return d&&X(V,n),{compacted:o,failed:f}}a(ks,"compactFullContexts");function R(e,{cwd:s,timeout:t=6e4}){const r=C(),i=Object.fromEntries(Object.entries(process.env).filter(([n])=>!D(n)));return i.CLAUDE_BG_ISOLATION="none",g.isAbsolute(r)&&(i.PATH=`${g.dirname(r)}:${i.PATH||""}`),new Promise(n=>{let o="",f="",d;try{d=J(r,e,{cwd:s,env:i,detached:!0,stdio:["ignore","pipe","pipe"]})}catch(l){n({status:1,stdout:o,stderr:l.message});return}const c=a(l=>{clearTimeout(p),d.stdout.destroy(),d.stderr.destroy(),n({status:l,stdout:o,stderr:f})},"done"),p=setTimeout(()=>{try{d.kill("SIGKILL")}catch{}c(1)},t);d.stdout.on("data",l=>{o+=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(R,"runClaude");async function Ke(e,{cwd:s,run:t=R,starts:r=xe,signal:i=process.kill}={}){if(e.kind==="bg"){const n=await t(["stop",e.jobId||e.sessionId.slice(0,8)],{cwd:s,timeout:8e3});if(n.status!==0)throw new m(`claude stop failed: ${(n.stderr||n.stdout).trim().slice(0,300)}`,"failed")}else{if(!je(e,r))throw new m(`Cannot verify session ${e.sessionId}'s process identity; refusing to signal pid ${e.pid}.`,"failed");i(e.pid,"SIGTERM")}}a(Ke,"runSessionStop");async function ys(e,{live:s=x,stop:t=Ke}={}){if(!e?.session)return;const r=s();if(!r)throw new m(oe(e.session),"unknown");const i=r.get(e.session);if(!i)return;await t(i,{cwd:e.repo||i.cwd||A.homedir()});const n=Date.now()+5e3;for(let o=s();!o||o.get(e.session)?.pid===i.pid;o=s()){if(Date.now()>=n)throw new m(`session ${e.session} did not stop`,"failed");await new Promise(f=>setTimeout(f,100))}}a(ys,"stopSession");function We(e){const s=`=${T(e)}`;return Je(Z(),process.env.TMUX?["switch-client","-t",s]:["attach-session","-t",s])}a(We,"attachTmux");function Je(e,s){const t=Object.fromEntries(Object.entries(process.env).filter(([n])=>!D(n)&&n!=="CLAUDE_BG_ISOLATION")),r=a(()=>{},"ignore");process.on("SIGINT",r).on("SIGQUIT",r);const i=a(()=>process.off("SIGINT",r).off("SIGQUIT",r),"settle");return new Promise((n,o)=>{const f=J(e,s,{env:t,stdio:"inherit"});f.on("error",d=>{i(),o(new m(d.message,"failed"))}),f.on("close",d=>{i(),n({status:d??1})})})}a(Je,"runForeground");const F=a(({name:e,model:s})=>["--name",e,"--permission-mode","auto","--settings",Fe,...s?["--model",s]:[]],"common"),He=a(e=>`'${e.replaceAll("'","'\\''")}'`,"quote"),L=a((e,s)=>`--name ${He(le(e,s))}`,"boardFlag"),Xe=a(({route:e,name:s,model:t,prompt:r})=>["--bg",...F({name:s,model:t}),`/kickflip ${L(e,s)}${r?` ${r}`:""}`],"startArgv"),qe=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 ${g.join(E,"bin","kickflip.mjs")} join ${L(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"),Qe=a(({id:e,route:s,name:t,model:r,background:i})=>["--resume",e,"--bg",...i?[]:F({name:t,model:r}),qe(s,e,t)],"resumeArgv"),ee=a((e,s)=>`You are the hub session. Read \`${g.join(E,"docs","hub-session.md")}\`, then rejoin /${e} (join ${L(e,s)}) and relaunch its watcher.`,"hubPrompt"),se=a(({route:e,title:s,id:t,model:r})=>[...t?["--resume",t]:[],...F({name:s,model:r}),t?`${ee(e,s)} If your session id (the folder above your scratchpad) isn't ${t}, rejoin with yours and --force.`:`/kickflip ${L(e,s)} --hub
2
- ${ee(e,s)}`],"hubArgv");function te(e,s,{cwd:t,tmux:r}){const i=C(),n=[...new Set(["CLAUDECODE","CLAUDE_BG_ISOLATION",...Object.keys(process.env).filter(D)])].flatMap(f=>["-u",f]),o=r(["new-session","-d","-s",T(e),"-c",t,"/usr/bin/env",...n,...g.isAbsolute(i)?[`PATH=${g.dirname(i)}:${process.env.PATH||""}`]:[],i,...s]);if(o.status!==0)throw new m(`tmux new-session failed (${o.status}): ${(o.stderr||o.stdout).trim().slice(0,300)}`,"failed");return[i,...s]}a(te,"startTmux");async function Is({manifest:e=_(),repo:s,tmux:t=B,attach:r=We,live:i=x}={}){const n=H(e);if(!n)throw new m("No hub board is flagged; flag one with kickflip set <route> --hub, or run kickflip join --hub in its repo folder","hub");if(!M(t,n.route)){const o=n.owner?.session?n.owner:null;if(o&&!o.repo&&!s)throw new m(`/${n.route}'s session ${o.session} has no folder recorded; pass --repo <its folder>`,"none");K(o,i);const f=o?.repo||g.resolve(s||process.cwd());if(!O.statSync(f,{throwIfNoEntry:!1})?.isDirectory())throw new m(o?.repo?`/${n.route}'s recorded folder ${f} is gone; rejoin from its new folder with --repo`:`no folder at ${f}`,"none");te(n.route,se({route:n.route,title:n.title,id:o?.session,model:o?.model}),{cwd:f,tmux:t})}return r(n.route)}a(Is,"hubSession");function G(e,s){const t=h(e?.sites,s),r=t?.owner?.session?t.owner:h(e?.starting,s);return r?.session?{...r,title:t?.title||s}:null}a(G,"sessionOf");async function ne(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 r=0;r<20;r++){const i=Te().find(n=>n.jobId===t||n.sessionId.startsWith(t));if(i)return{session:i.sessionId,confirmed:!0};await new Promise(n=>setTimeout(n,500))}return{session:s,confirmed:!1}}a(ne,"startedId");async function Ye(e,{run:s,cwd:t}){const r=await s(["agents","--json","--all"],{cwd:t});try{return JSON.parse(r.stdout).some(i=>i?.sessionId===e&&i.kind==="background")}catch{return!1}}a(Ye,"isBackground");const oe=a(e=>`Couldn't tell whether session ${e} is live: ps gave no answer in time, as happens on a busy Mac. Try again in a moment.`,"unknownLiveness");function K(e,s){if(!e)return;const t=s();if(!t)throw new m(oe(e.session),"unknown");const r=t.get(e.session);if(r)throw new m(`session ${e.session} is live (${r.kind==="bg"?"in the background":"in a terminal"}, pid ${r.pid}); it gets no second process`,"live")}a(K,"refuseLive");async function ie(e,{from:s,to:t,mark:r}){await ye(async()=>{const i=_(),n=U(i).find(f=>f.route===e);if(n?.owner?.session===s&&(r||t!==s)&&me(n,{from:s,to:t,mark:r}),t===s)return;const o=h(i.sites,e);if(o?.owner?.session===s)Object.assign(o,{owner:{...o.owner,session:t},updated:new Date().toISOString()});else if(h(i.starting,e)?.session===s)i.starting[e]={...h(i.starting,e),session:t};else return;await Ie(i)})}a(ie,"moveOwner");async function Ss(e,{manifest:s=_(),repo:t,model:r,prompt:i,run:n=R,live:o=x,clones:f=Ae()}={}){if(h(s.closed,e))throw new m(`/${e} is closed; use kickflip reopen ${e}`,"none");const d=G(s,e);K(d,o);const c=g.resolve(t||d?.repo||process.cwd());if(!O.statSync(c,{throwIfNoEntry:!1})?.isDirectory())throw new m(`no folder at ${c}`,"none");if(Ee(c,f))try{Ce(c)}catch(w){throw new m(`Couldn't trust this folder for Claude Code: ${w.message}`,"failed")}const p=h(s?.sites,e)?.title||h(s?.starting,e)?.title||e,l=Xe({route:e,name:p,model:r||d?.model,prompt:i}),u=await n(l,{cwd:c});if(u.status!==0){const w=(u.stderr||"").trim()||(u.stdout||"").trim();throw new m(w?`claude --bg failed (${u.status}): ${w.slice(0,300)}`:`claude exited ${u.status} with no output; check \`claude agents\``,"failed")}const{session:y,confirmed:b}=await ne(u.stdout);if(!y)throw new m(`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:y,confirmed:b,repo:Q(c),model:r||d?.model||null,name:p,argv:[C(),...l],output:u.stdout.trim()}}a(Ss,"startSession");function ze(e,s={}){return Ge(e,({resume:t})=>t(s))}a(ze,"resumeSession");async function Ve(e,{manifest:s=_(),run:t=R,tmux:r=B,live:i=x,mark:n=null}={}){if(h(_().closed,e))throw new m(`/${e} is closed; use kickflip reopen ${e}`,"none");const o=G(s,e);if(!o)throw new m(`/${e} has no recorded session`,"none");if(!o.repo)throw new m(`/${e}'s session ${o.session} has no folder recorded; its session records one by rejoining (kickflip join, --repo)`,"none");if(K(o,i),h(s.sites,e)?.hub===!0){if(M(r,e))throw new m(`/${e}'s tmux session ${T(e)} is still open; attach with kickflip hub`,"live");const u=te(e,se({route:e,title:o.title,id:o.session,model:o.model}),{cwd:o.repo,tmux:r});return await ie(e,{from:o.session,to:o.session,mark:n}),{route:e,session:o.session,previous:o.session,confirmed:!1,repo:o.repo,argv:u,output:`in tmux session ${T(e)}`}}const f=await Ye(o.session,{run:t,cwd:o.repo}),d=Qe({id:o.session,route:e,name:o.title,model:o.model,background:f}),c=await t(d,{cwd:o.repo});if(c.status!==0)throw new m(`claude --resume failed (${c.status}): ${(c.stderr||c.stdout).trim().slice(0,300)}`,"failed");const{session:p,confirmed:l}=await ne(c.stdout,o.session);return await ie(e,{from:o.session,to:p,mark:n}),{route:e,session:p,previous:o.session,confirmed:l,repo:o.repo,argv:[C(),...d],output:c.stdout.trim()}}a(Ve,"resumeRecordedSession");function Ze(e,{listening:s,live:t,recorded:r,counts:i,waiting:n,asked:o=!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)>=Ue?{action:"push",entry:{...c,pushed_at:d}}:{action:"none",entry:c}:!r||t?{action:"none",entry:c}:!!(e?.mark&&i&&Object.entries(i).some(([l,u])=>u>(h(e.mark,l)??0)))||o||n&&f-Date.parse(c.down_since)>=Pe?{action:"resume",entry:{...c,resumed_at:d}}:{action:"none",entry:c}}a(Ze,"resumeStep");async function _s({manifest:e=_(),now:s=new Date,live:t=x,asked:r=new Set,listening:i=a((d,c)=>he(d,{now:s.getTime(),running:c}),"listening"),resume:n=a((d,c)=>ze(d,{manifest:e,live:t,mark:c}),"resume"),push:o=ve,gateOf:f=a(d=>Se({now:s,dir:d.dir}),"gateOf")}={}){const d=t();if(!d)return{resumed:[],pushed:[],failed:[]};const c=N(P,{}),p=Object.create(null),l={resumed:[],pushed:[],failed:[]};for(const u of U(e)){const y=G(e,u.route),b=!!(y&&d.has(y.session)),w=i(u,d);let{action:j,entry:$}=Ze(h(c,u.route),{listening:w,live:b,recorded:!!y?.repo,now:s.getTime(),asked:r.has(u.route),counts:w?null:be(u),waiting:!w&&!b&&ge(u)!==null});if(j==="resume"&&f(u).state==="paused"){const I=h(c,u.route)?.mark||ke(u);Object.entries($e(u)).flatMap(([S,ae])=>ae.slice(h(I,S)||0)).some(S=>!_e(S))||(j="none",delete $.resumed_at)}const W=h(c,u.route),re={listening:w,live:b,asked:r.has(u.route),downSince:W?.down_since??null,resumedAt:W?.resumed_at??null},v=a((I,k,S)=>Y(z(u.dir),{call:I,result:k,reason:S,inputs:re},{now:s}),"logged");if(j==="resume"){const I=r.has(u.route)?"The owner opened the board's page while its session wasn't listening.":"An event waited while the board's session wasn't listening.";try{await n(u.route,$.mark),l.resumed.push(u.route),v("session-resume","resumed",I)}catch(k){k.reason==="unknown"?delete $.resumed_at:$={...$,error:k.message},l.failed.push(`${u.route}: could not resume its session (${k.message})`),v("session-resume","failed",`${I} ${k.message}`)}}else if(j==="push"){const I=$.error?`The hub couldn't resume it: ${$.error}`:b?"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.`,k=`Its session didn't come back after the hub resumed it: ${I}`;try{await o({title:`${u.title}'s session didn't come back`,body:I,url:`/${u.route}/`,tag:`resume-${u.route}`},{kind:"fault",immediate:!0,at:s}),l.pushed.push(u.route),v("push","sent",k)}catch(S){$={...$,pushed_at:void 0},l.failed.push(`${u.route}: could not push that its session didn't come back (${S.message})`),v("push","not sent",`${k} Not sent: ${S.message}`)}}$&&(p[u.route]=$)}return JSON.stringify(p)!==JSON.stringify(c)&&X(P,p),l}a(_s,"checkSessions");export{Fe as BG_SETTINGS,Ue as COMEBACK_WAIT,Re as PROMPT_HOOK_TIMEOUT_S,Pe as RESUME_AFTER,Ne as STARTING_WAIT,m as SessionError,We as attachTmux,_s as checkSessions,C as claudeBin,ks as compactFullContexts,se as hubArgv,Is as hubSession,D as inherited,Qe as resumeArgv,qe as resumePrompt,ze as resumeSession,Ze as resumeStep,R as runClaude,Ke as runSessionStop,B as runTmux,G as sessionOf,Xe as startArgv,Be as startFailed,Ss as startSession,$s as startingBoards,ys as stopSession,Z as tmuxBin,T as tmuxName,Ge as withSessionLock};
1
+ var ce=Object.defineProperty;var a=(e,s)=>ce(e,"name",{value:s,configurable:!0});import O from"node:fs";import g from"node:path";import A from"node:os";import{spawn as J,spawnSync as de}from"node:child_process";import{fileURLToPath as ue}from"node:url";import{joinName as le,routeRecord as h,validRoute as fe}from"./routes.mjs";import{withFileLock as pe}from"./locks.mjs";import{RESUME_STATE as P,carryCursor as me,hubBoard as H,isListening as he,listBoards as U,listenState as we,oldestUnheard as ge,readManifest as _,readJSON as N,saveJSON as X,watchedCounts as be,watchedLines as $e,watcherCursor as ke,withManifestLock as ye,writeManifest as Ie}from"./boards.mjs";import{readUsageGate as Se,heldUsageEvent as _e}from"../shared/usage-gate.mjs";import{liveSessions as x,sessionRecords as Te,processStarts as xe,verifiedProcess as je}from"./live.mjs";import{deliver as ve}from"./push.mjs";import{WAIT_MS as Oe}from"./prompts.mjs";import{nodeBin as q,storedPath as Q}from"../bin/install.mjs";import{reposDir as Ae}from"../shared/repos.mjs";import{isClone as Ee}from"./repos.mjs";import{trustFolder as Ce}from"./trust.mjs";import{contextFull as De,contextReading as Le}from"./usage.mjs";import{audit as Y,boardAudit as z}from"../shared/audit.mjs";const E=Q(g.resolve(g.dirname(ue(import.meta.url)),"..")),Pe=3*6e4,Ue=5*6e4,Ne=15*6e4,V=g.join(A.homedir(),".claude","kickflip","compact-state.json"),Be=a((e,s=Date.now())=>!!e.error||s-Date.parse(e.asked_at||e.since)>=Ne,"startFailed");function Re(e){return e.joined_at?{step:"landing",stepAt:e.joined_at}:e.phase==="cloning"?{step:"cloning",stepAt:e.asked_at}:{step:"starting",stepAt:e.phase_at||e.asked_at||e.since}}a(Re,"startStep");function $s(e,s=Date.now(),t=()=>{},r=()=>!0){return Object.entries(e.starting||{}).filter(([i,n])=>h(e.sites,i)?n.joined_at&&!r(i):!0).map(([i,n])=>{const o=Be(n,s);return{route:i,title:n.title||n.label||i,repo:n.repo,...Re(n),...n.phase==="cloning"&&t(i)!==void 0?{percent:t(i)}:{},...n.branch?{branch:n.branch,tickets:n.tickets}:{},askedAt:n.asked_at||n.since,failed:o,...n.joined_at?{joined:!0}:{},...n.error?{error:n.error}:o&&n.joined_at?{error:"Its session joined, but the board is not listening yet."}:{}}})}a($s,"startingBoards");const Me=Oe/1e3+60,Fe=JSON.stringify({worktree:{bgIsolation:"none"},permissions:{deny:["Workflow"]},hooks:{PermissionRequest:[{matcher:"*",hooks:[{type:"command",command:`"${q()}" "${g.join(E,"hub","permission-hook.mjs")}"`,timeout:Me}]}],PermissionDenied:[{matcher:"Bash",hooks:[{type:"command",command:`"${q()}" "${g.join(E,"hub","permission-denied-hook.mjs")}"`,timeout:15}]}]}});class m extends Error{static{a(this,"SessionError")}constructor(s,t){super(s),this.reason=t}}function Ge(e,s){if(!fe(e))throw new m("Choose a board route.","none");return pe(g.join(A.homedir(),".claude","kickflip","session-locks",e),()=>s({resume:a(t=>Ve(e,t),"resume")}),{staleAfter:12e4,timeout:9e4})}a(Ge,"withSessionLock");function C(){if(process.env.KICKFLIP_CLAUDE)return process.env.KICKFLIP_CLAUDE;const e=g.join(A.homedir(),".local","bin","claude");return O.existsSync(e)?e:"claude"}a(C,"claudeBin");const D=a(e=>e==="CLAUDECODE"||e==="CLAUDE_PID"||e==="CLAUDE_EFFORT"||e.startsWith("CLAUDE_CODE_"),"inherited");function Z(){return process.env.KICKFLIP_TMUX?process.env.KICKFLIP_TMUX:["/opt/homebrew/bin/tmux","/usr/local/bin/tmux","/usr/bin/tmux"].find(e=>O.existsSync(e))||"tmux"}a(Z,"tmuxBin");function B(e){const s=de(Z(),e,{encoding:"utf8",timeout:1e4,env:Object.fromEntries(Object.entries(process.env).filter(([t])=>!D(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(B,"runTmux");const T=a(e=>`kickflip-${e}`,"tmuxName"),R=a((e,s)=>e(["has-session","-t",`=${T(s)}`]).status===0,"hasTmux");async function ks({manifest:e=_(),now:s=new Date,running:t=x(),wakes:r=N(P,{}),tmux:i=B}={}){if(!t)return{compacted:[],failed:[]};const n=Object.assign(Object.create(null),N(V,{})),o=[],f=[];let d=!1;const c=H(e)?.route;for(const p of U(e)){const l=Le(p.owner?.session);if(!De(we(p,{now:s.getTime(),running:t,wakes:r}),l)){h(n,p.route)&&(delete n[p.route],d=!0);continue}if(p.route!==c||h(n,p.route)||!R(i,p.route))continue;const u=`=${T(p.route)}:`,y=i(["send-keys","-t",u,"-l","/compact"]),b=y.status===0?i(["send-keys","-t",u,"Enter"]):y,w=b.status===0?null:(b.stderr||b.stdout).trim().slice(0,300)||`tmux exited ${b.status}`;n[p.route]={session:p.owner.session,percent:l.percent,at:s.toISOString(),...w?{error:w}:{}},Y(z(p.dir),{call:"compact",result:w?"failed":"typed /compact",reason:`Its session's context is ${l.percent}% full and it isn't listening.${w?` ${w}`:""}`,inputs:{session:p.owner.session,context:l}},{now:s}),d=!0,w?f.push(`${p.route}: could not send /compact to tmux session ${T(p.route)} (${w})`):o.push(p.route)}return d&&X(V,n),{compacted:o,failed:f}}a(ks,"compactFullContexts");function M(e,{cwd:s,timeout:t=6e4}){const r=C(),i=Object.fromEntries(Object.entries(process.env).filter(([n])=>!D(n)));return i.CLAUDE_BG_ISOLATION="none",g.isAbsolute(r)&&(i.PATH=`${g.dirname(r)}:${i.PATH||""}`),new Promise(n=>{let o="",f="",d;try{d=J(r,e,{cwd:s,env:i,detached:!0,stdio:["ignore","pipe","pipe"]})}catch(l){n({status:1,stdout:o,stderr:l.message});return}const c=a(l=>{clearTimeout(p),d.stdout.destroy(),d.stderr.destroy(),n({status:l,stdout:o,stderr:f})},"done"),p=setTimeout(()=>{try{d.kill("SIGKILL")}catch{}c(1)},t);d.stdout.on("data",l=>{o+=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(M,"runClaude");async function Ke(e,{cwd:s,run:t=M,starts:r=xe,signal:i=process.kill}={}){if(e.kind==="bg"){const n=await t(["stop",e.jobId||e.sessionId.slice(0,8)],{cwd:s,timeout:8e3});if(n.status!==0)throw new m(`claude stop failed: ${(n.stderr||n.stdout).trim().slice(0,300)}`,"failed")}else{if(!je(e,r))throw new m(`Cannot verify session ${e.sessionId}'s process identity; refusing to signal pid ${e.pid}.`,"failed");i(e.pid,"SIGTERM")}}a(Ke,"runSessionStop");async function ys(e,{live:s=x,stop:t=Ke}={}){if(!e?.session)return;const r=s();if(!r)throw new m(oe(e.session),"unknown");const i=r.get(e.session);if(!i)return;await t(i,{cwd:e.repo||i.cwd||A.homedir()});const n=Date.now()+5e3;for(let o=s();!o||o.get(e.session)?.pid===i.pid;o=s()){if(Date.now()>=n)throw new m(`session ${e.session} did not stop`,"failed");await new Promise(f=>setTimeout(f,100))}}a(ys,"stopSession");function We(e){const s=`=${T(e)}`;return Je(Z(),process.env.TMUX?["switch-client","-t",s]:["attach-session","-t",s])}a(We,"attachTmux");function Je(e,s){const t=Object.fromEntries(Object.entries(process.env).filter(([n])=>!D(n)&&n!=="CLAUDE_BG_ISOLATION")),r=a(()=>{},"ignore");process.on("SIGINT",r).on("SIGQUIT",r);const i=a(()=>process.off("SIGINT",r).off("SIGQUIT",r),"settle");return new Promise((n,o)=>{const f=J(e,s,{env:t,stdio:"inherit"});f.on("error",d=>{i(),o(new m(d.message,"failed"))}),f.on("close",d=>{i(),n({status:d??1})})})}a(Je,"runForeground");const F=a(({name:e,model:s})=>["--name",e,"--permission-mode","auto","--settings",Fe,...s?["--model",s]:[]],"common"),He=a(e=>`'${e.replaceAll("'","'\\''")}'`,"quote"),L=a((e,s)=>`--name ${He(le(e,s))}`,"boardFlag"),Xe=a(({route:e,name:s,model:t,prompt:r})=>["--bg",...F({name:s,model:t}),`/kickflip ${L(e,s)}${r?` ${r}`:""}`],"startArgv"),qe=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 ${g.join(E,"bin","kickflip.mjs")} join ${L(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"),Qe=a(({id:e,route:s,name:t,model:r,background:i})=>["--resume",e,"--bg",...i?[]:F({name:t,model:r}),qe(s,e,t)],"resumeArgv"),ee=a((e,s)=>`You are the hub session. Read \`${g.join(E,"docs","hub-session.md")}\`, then rejoin /${e} (join ${L(e,s)}) and relaunch its watcher.`,"hubPrompt"),se=a(({route:e,title:s,id:t,model:r})=>[...t?["--resume",t]:[],...F({name:s,model:r}),t?`${ee(e,s)} If your session id (the folder above your scratchpad) isn't ${t}, rejoin with yours and --force.`:`/kickflip ${L(e,s)} --hub
2
+ ${ee(e,s)}`],"hubArgv");function te(e,s,{cwd:t,tmux:r}){const i=C(),n=[...new Set(["CLAUDECODE","CLAUDE_BG_ISOLATION",...Object.keys(process.env).filter(D)])].flatMap(f=>["-u",f]),o=r(["new-session","-d","-s",T(e),"-c",t,"/usr/bin/env",...n,...g.isAbsolute(i)?[`PATH=${g.dirname(i)}:${process.env.PATH||""}`]:[],i,...s]);if(o.status!==0)throw new m(`tmux new-session failed (${o.status}): ${(o.stderr||o.stdout).trim().slice(0,300)}`,"failed");return[i,...s]}a(te,"startTmux");async function Is({manifest:e=_(),repo:s,tmux:t=B,attach:r=We,live:i=x}={}){const n=H(e);if(!n)throw new m("No hub board is flagged; flag one with kickflip set <route> --hub, or run kickflip join --hub in its repo folder","hub");if(!R(t,n.route)){const o=n.owner?.session?n.owner:null;if(o&&!o.repo&&!s)throw new m(`/${n.route}'s session ${o.session} has no folder recorded; pass --repo <its folder>`,"none");K(o,i);const f=o?.repo||g.resolve(s||process.cwd());if(!O.statSync(f,{throwIfNoEntry:!1})?.isDirectory())throw new m(o?.repo?`/${n.route}'s recorded folder ${f} is gone; rejoin from its new folder with --repo`:`no folder at ${f}`,"none");te(n.route,se({route:n.route,title:n.title,id:o?.session,model:o?.model}),{cwd:f,tmux:t})}return r(n.route)}a(Is,"hubSession");function G(e,s){const t=h(e?.sites,s),r=t?.owner?.session?t.owner:h(e?.starting,s);return r?.session?{...r,title:t?.title||s}:null}a(G,"sessionOf");async function ne(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 r=0;r<20;r++){const i=Te().find(n=>n.jobId===t||n.sessionId.startsWith(t));if(i)return{session:i.sessionId,confirmed:!0};await new Promise(n=>setTimeout(n,500))}return{session:s,confirmed:!1}}a(ne,"startedId");async function Ye(e,{run:s,cwd:t}){const r=await s(["agents","--json","--all"],{cwd:t});try{return JSON.parse(r.stdout).some(i=>i?.sessionId===e&&i.kind==="background")}catch{return!1}}a(Ye,"isBackground");const oe=a(e=>`Couldn't tell whether session ${e} is live: ps gave no answer in time, as happens on a busy host. Try again in a moment.`,"unknownLiveness");function K(e,s){if(!e)return;const t=s();if(!t)throw new m(oe(e.session),"unknown");const r=t.get(e.session);if(r)throw new m(`session ${e.session} is live (${r.kind==="bg"?"in the background":"in a terminal"}, pid ${r.pid}); it gets no second process`,"live")}a(K,"refuseLive");async function ie(e,{from:s,to:t,mark:r}){await ye(async()=>{const i=_(),n=U(i).find(f=>f.route===e);if(n?.owner?.session===s&&(r||t!==s)&&me(n,{from:s,to:t,mark:r}),t===s)return;const o=h(i.sites,e);if(o?.owner?.session===s)Object.assign(o,{owner:{...o.owner,session:t},updated:new Date().toISOString()});else if(h(i.starting,e)?.session===s)i.starting[e]={...h(i.starting,e),session:t};else return;await Ie(i)})}a(ie,"moveOwner");async function Ss(e,{manifest:s=_(),repo:t,model:r,prompt:i,run:n=M,live:o=x,clones:f=Ae()}={}){if(h(s.closed,e))throw new m(`/${e} is closed; use kickflip reopen ${e}`,"none");const d=G(s,e);K(d,o);const c=g.resolve(t||d?.repo||process.cwd());if(!O.statSync(c,{throwIfNoEntry:!1})?.isDirectory())throw new m(`no folder at ${c}`,"none");if(Ee(c,f))try{Ce(c)}catch(w){throw new m(`Couldn't trust this folder for Claude Code: ${w.message}`,"failed")}const p=h(s?.sites,e)?.title||h(s?.starting,e)?.title||e,l=Xe({route:e,name:p,model:r||d?.model,prompt:i}),u=await n(l,{cwd:c});if(u.status!==0){const w=(u.stderr||"").trim()||(u.stdout||"").trim();throw new m(w?`claude --bg failed (${u.status}): ${w.slice(0,300)}`:`claude exited ${u.status} with no output; check \`claude agents\``,"failed")}const{session:y,confirmed:b}=await ne(u.stdout);if(!y)throw new m(`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:y,confirmed:b,repo:Q(c),model:r||d?.model||null,name:p,argv:[C(),...l],output:u.stdout.trim()}}a(Ss,"startSession");function ze(e,s={}){return Ge(e,({resume:t})=>t(s))}a(ze,"resumeSession");async function Ve(e,{manifest:s=_(),run:t=M,tmux:r=B,live:i=x,mark:n=null}={}){if(h(_().closed,e))throw new m(`/${e} is closed; use kickflip reopen ${e}`,"none");const o=G(s,e);if(!o)throw new m(`/${e} has no recorded session`,"none");if(!o.repo)throw new m(`/${e}'s session ${o.session} has no folder recorded; its session records one by rejoining (kickflip join, --repo)`,"none");if(K(o,i),h(s.sites,e)?.hub===!0){if(R(r,e))throw new m(`/${e}'s tmux session ${T(e)} is still open; attach with kickflip hub`,"live");const u=te(e,se({route:e,title:o.title,id:o.session,model:o.model}),{cwd:o.repo,tmux:r});return await ie(e,{from:o.session,to:o.session,mark:n}),{route:e,session:o.session,previous:o.session,confirmed:!1,repo:o.repo,argv:u,output:`in tmux session ${T(e)}`}}const f=await Ye(o.session,{run:t,cwd:o.repo}),d=Qe({id:o.session,route:e,name:o.title,model:o.model,background:f}),c=await t(d,{cwd:o.repo});if(c.status!==0)throw new m(`claude --resume failed (${c.status}): ${(c.stderr||c.stdout).trim().slice(0,300)}`,"failed");const{session:p,confirmed:l}=await ne(c.stdout,o.session);return await ie(e,{from:o.session,to:p,mark:n}),{route:e,session:p,previous:o.session,confirmed:l,repo:o.repo,argv:[C(),...d],output:c.stdout.trim()}}a(Ve,"resumeRecordedSession");function Ze(e,{listening:s,live:t,recorded:r,counts:i,waiting:n,asked:o=!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)>=Ue?{action:"push",entry:{...c,pushed_at:d}}:{action:"none",entry:c}:!r||t?{action:"none",entry:c}:!!(e?.mark&&i&&Object.entries(i).some(([l,u])=>u>(h(e.mark,l)??0)))||o||n&&f-Date.parse(c.down_since)>=Pe?{action:"resume",entry:{...c,resumed_at:d}}:{action:"none",entry:c}}a(Ze,"resumeStep");async function _s({manifest:e=_(),now:s=new Date,live:t=x,asked:r=new Set,listening:i=a((d,c)=>he(d,{now:s.getTime(),running:c}),"listening"),resume:n=a((d,c)=>ze(d,{manifest:e,live:t,mark:c}),"resume"),push:o=ve,gateOf:f=a(d=>Se({now:s,dir:d.dir}),"gateOf")}={}){const d=t();if(!d)return{resumed:[],pushed:[],failed:[]};const c=N(P,{}),p=Object.create(null),l={resumed:[],pushed:[],failed:[]};for(const u of U(e)){const y=G(e,u.route),b=!!(y&&d.has(y.session)),w=i(u,d);let{action:j,entry:$}=Ze(h(c,u.route),{listening:w,live:b,recorded:!!y?.repo,now:s.getTime(),asked:r.has(u.route),counts:w?null:be(u),waiting:!w&&!b&&ge(u)!==null});if(j==="resume"&&f(u).state==="paused"){const I=h(c,u.route)?.mark||ke(u);Object.entries($e(u)).flatMap(([S,ae])=>ae.slice(h(I,S)||0)).some(S=>!_e(S))||(j="none",delete $.resumed_at)}const W=h(c,u.route),re={listening:w,live:b,asked:r.has(u.route),downSince:W?.down_since??null,resumedAt:W?.resumed_at??null},v=a((I,k,S)=>Y(z(u.dir),{call:I,result:k,reason:S,inputs:re},{now:s}),"logged");if(j==="resume"){const I=r.has(u.route)?"The owner opened the board's page while its session wasn't listening.":"An event waited while the board's session wasn't listening.";try{await n(u.route,$.mark),l.resumed.push(u.route),v("session-resume","resumed",I)}catch(k){k.reason==="unknown"?delete $.resumed_at:$={...$,error:k.message},l.failed.push(`${u.route}: could not resume its session (${k.message})`),v("session-resume","failed",`${I} ${k.message}`)}}else if(j==="push"){const I=$.error?`The hub couldn't resume it: ${$.error}`:b?"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.`,k=`Its session didn't come back after the hub resumed it: ${I}`;try{await o({title:`${u.title}'s session didn't come back`,body:I,url:`/${u.route}/`,tag:`resume-${u.route}`},{kind:"fault",immediate:!0,at:s}),l.pushed.push(u.route),v("push","sent",k)}catch(S){$={...$,pushed_at:void 0},l.failed.push(`${u.route}: could not push that its session didn't come back (${S.message})`),v("push","not sent",`${k} Not sent: ${S.message}`)}}$&&(p[u.route]=$)}return JSON.stringify(p)!==JSON.stringify(c)&&X(P,p),l}a(_s,"checkSessions");export{Fe as BG_SETTINGS,Ue as COMEBACK_WAIT,Me as PROMPT_HOOK_TIMEOUT_S,Pe as RESUME_AFTER,Ne as STARTING_WAIT,m as SessionError,We as attachTmux,_s as checkSessions,C as claudeBin,ks as compactFullContexts,se as hubArgv,Is as hubSession,D as inherited,Qe as resumeArgv,qe as resumePrompt,ze as resumeSession,Ze as resumeStep,M as runClaude,Ke as runSessionStop,B as runTmux,G as sessionOf,Xe as startArgv,Be as startFailed,Ss as startSession,$s as startingBoards,ys as stopSession,Z as tmuxBin,T as tmuxName,Ge as withSessionLock};