@lovek4d/kickflip 0.0.0-stage → 0.1.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 (111) hide show
  1. package/CONTEXT.md +414 -0
  2. package/LICENSE +57 -0
  3. package/README.md +23 -2
  4. package/SKILL.md +121 -0
  5. package/bin/doctor.mjs +3 -0
  6. package/bin/install.mjs +3 -0
  7. package/bin/kickflip.mjs +125 -0
  8. package/bin/statusline.mjs +4 -0
  9. package/bin/wait-events.mjs +3 -0
  10. package/bin/web-ts.mjs +2 -0
  11. package/board/bin/agent.mjs +2 -0
  12. package/board/bin/board.mjs +207 -0
  13. package/board/bin/builder-stop-hook.mjs +5 -0
  14. package/board/build-script.mjs +1 -0
  15. package/board/domain.mjs +3 -0
  16. package/board/public/app-asks.js +169 -0
  17. package/board/public/app-banner.js +48 -0
  18. package/board/public/app-cards.js +200 -0
  19. package/board/public/app-core.js +96 -0
  20. package/board/public/app-header.js +108 -0
  21. package/board/public/app-lanes.js +92 -0
  22. package/board/public/app-needs.js +30 -0
  23. package/board/public/app-snapshot.js +97 -0
  24. package/board/public/app-threads.js +165 -0
  25. package/board/public/app.html +83 -0
  26. package/board/public/app.js +327 -0
  27. package/board/public/attach.js +84 -0
  28. package/board/public/dialogs.js +360 -0
  29. package/board/public/hub-kit.js +151 -0
  30. package/board/public/hub.css +164 -0
  31. package/board/public/kickflip.png +0 -0
  32. package/board/public/prompts.js +64 -0
  33. package/board/public/prototype-kit.js +193 -0
  34. package/board/public/prototype-switcher.js +226 -0
  35. package/board/public/prototype.html +55 -0
  36. package/board/public/router.js +223 -0
  37. package/board/public/rules-editor.js +127 -0
  38. package/board/public/shell.js +298 -0
  39. package/board/public/sidebar.css +199 -0
  40. package/board/public/sidebar.js +287 -0
  41. package/board/public/style.css +570 -0
  42. package/board/public/toasts.js +179 -0
  43. package/board/public/ui-cards.js +153 -0
  44. package/board/public/ui-enums.js +10 -0
  45. package/board/public/ui-header.js +84 -0
  46. package/board/public/ui-hub.js +94 -0
  47. package/board/public/ui-links.js +142 -0
  48. package/board/public/ui-markdown.js +80 -0
  49. package/board/public/ui-page-header.js +63 -0
  50. package/board/public/ui-prompts.js +39 -0
  51. package/board/public/ui-questions.js +130 -0
  52. package/board/public/ui-sidebar.js +177 -0
  53. package/board/public/ui-snapshot.js +56 -0
  54. package/board/public/ui-threads.js +81 -0
  55. package/board/public/ui.js +235 -0
  56. package/board/public/view-frame.js +93 -0
  57. package/board/public/view-hub-settings.js +266 -0
  58. package/board/public/view-hub.js +99 -0
  59. package/board/public/view-notes.js +29 -0
  60. package/board/public/view-polling.js +87 -0
  61. package/board/public/view-questions.js +391 -0
  62. package/board/public/view-settings.js +328 -0
  63. package/board/server.mjs +12 -0
  64. package/board/store.mjs +7 -0
  65. package/board/tickets.mjs +3 -0
  66. package/board/worktree.mjs +14 -0
  67. package/docs/hub-session.md +11 -0
  68. package/hooks-plugin/.claude-plugin/plugin.json +5 -0
  69. package/hooks-plugin/hooks/codex-share-auto.mjs +21 -0
  70. package/hooks-plugin/hooks/hooks.json +54 -0
  71. package/hooks-plugin/hooks/usage-gate.mjs +18 -0
  72. package/hub/board-repo.mjs +1 -0
  73. package/hub/boards.mjs +4 -0
  74. package/hub/close-board.mjs +1 -0
  75. package/hub/files.mjs +1 -0
  76. package/hub/github.mjs +5 -0
  77. package/hub/home.mjs +1 -0
  78. package/hub/hub.mjs +17 -0
  79. package/hub/icon-180.png +0 -0
  80. package/hub/icon-192.png +0 -0
  81. package/hub/icon-512.png +0 -0
  82. package/hub/icons.mjs +1 -0
  83. package/hub/linear.mjs +2 -0
  84. package/hub/live.mjs +2 -0
  85. package/hub/locks.mjs +1 -0
  86. package/hub/new-board.mjs +2 -0
  87. package/hub/permission-hook.mjs +3 -0
  88. package/hub/presence.mjs +1 -0
  89. package/hub/prompts.mjs +2 -0
  90. package/hub/push.mjs +2 -0
  91. package/hub/repos.mjs +2 -0
  92. package/hub/routes.mjs +1 -0
  93. package/hub/rules.mjs +1 -0
  94. package/hub/schedule.mjs +1 -0
  95. package/hub/sessions.mjs +2 -0
  96. package/hub/shell.mjs +1 -0
  97. package/hub/stream.mjs +19 -0
  98. package/hub/sw.js +144 -0
  99. package/hub/try.mjs +3 -0
  100. package/hub/usage.mjs +3 -0
  101. package/hub/views.mjs +1 -0
  102. package/package.json +26 -4
  103. package/shared/codex.mjs +1 -0
  104. package/shared/colors.css +13 -0
  105. package/shared/repos.mjs +1 -0
  106. package/shared/service.mjs +33 -0
  107. package/shared/usage-gate.mjs +1 -0
  108. package/starter/cross-site.mjs +1 -0
  109. package/starter/public/claude.js +67 -0
  110. package/starter/public/index.html +49 -0
  111. package/starter/server.mjs +4 -0
@@ -0,0 +1,21 @@
1
+ #!/usr/bin/env node
2
+ // SessionStart and UserPromptSubmit: the hub setting is the default for every session.
3
+ import fs from 'node:fs';
4
+ import os from 'node:os';
5
+ import path from 'node:path';
6
+ import { codexStatus, effectiveCodexShare } from '../../shared/codex.mjs';
7
+ import { readUsageGate } from '../../shared/usage-gate.mjs';
8
+
9
+ process.stdout.on('error', () => {});
10
+ try {
11
+ let stored;
12
+ try { stored = JSON.parse(fs.readFileSync(path.join(os.homedir(), '.claude', 'kickflip', 'settings.json'), 'utf8')).codexShare; } catch {}
13
+ const codexShare = effectiveCodexShare(stored, codexStatus());
14
+ const line = readUsageGate().pause_at;
15
+ if (codexShare === 'auto') {
16
+ console.log(`The hub's Codex setting is auto: unless this session's codex-share-mode file says otherwise, run as codex-share auto. Hand implementation and review to Codex as ~/.claude/skills/codex-share/SKILL.md describes (read it first); while Codex is past the pause line (${line}%) or out of usage, Opus builders take over until it resets. Say so in one line the first time it applies in this session.`);
17
+ } else if (codexShare === 'always') {
18
+ console.log(`The hub's Codex setting is always on: unless this session's codex-share-mode file says otherwise, run as codex-share on. Hand implementation and review to Codex as ~/.claude/skills/codex-share/SKILL.md describes (read it first); while Codex is past the pause line (${line}%), Opus builders take over until it resets, and while Codex is out of usage, pause that work until the usage-reset probe answers. Say so in one line the first time it applies in this session.`);
19
+ }
20
+ } catch { /* Errors are silent and never fail the session. */ }
21
+ process.exitCode = 0;
@@ -0,0 +1,54 @@
1
+ {
2
+ "hooks": {
3
+ "SessionStart": [
4
+ {
5
+ "hooks": [
6
+ {
7
+ "type": "command",
8
+ "command": "node \"$HOME/.claude/skills/kickflip/hooks-plugin/hooks/codex-share-auto.mjs\""
9
+ },
10
+ {
11
+ "type": "command",
12
+ "command": "node \"$HOME/.claude/skills/kickflip/hooks-plugin/hooks/usage-gate.mjs\""
13
+ }
14
+ ]
15
+ }
16
+ ],
17
+ "UserPromptSubmit": [
18
+ {
19
+ "hooks": [
20
+ {
21
+ "type": "command",
22
+ "command": "node \"$HOME/.claude/skills/kickflip/hooks-plugin/hooks/codex-share-auto.mjs\""
23
+ },
24
+ {
25
+ "type": "command",
26
+ "command": "node \"$HOME/.claude/skills/kickflip/hooks-plugin/hooks/usage-gate.mjs\""
27
+ }
28
+ ]
29
+ }
30
+ ],
31
+ "SubagentStop": [
32
+ {
33
+ "hooks": [
34
+ {
35
+ "type": "command",
36
+ "command": "node \"$HOME/.claude/skills/kickflip/board/bin/builder-stop-hook.mjs\"",
37
+ "async": true
38
+ }
39
+ ]
40
+ }
41
+ ],
42
+ "PreToolUse": [
43
+ {
44
+ "matcher": "Agent|Task",
45
+ "hooks": [
46
+ {
47
+ "type": "command",
48
+ "command": "node \"$HOME/.claude/skills/kickflip/hooks-plugin/hooks/usage-gate.mjs\""
49
+ }
50
+ ]
51
+ }
52
+ ]
53
+ }
54
+ }
@@ -0,0 +1,18 @@
1
+ #!/usr/bin/env node
2
+ import fs from 'node:fs';
3
+ import { readUsageGate, pauseReason } from '../../shared/usage-gate.mjs';
4
+
5
+ process.stdout.on('error', () => {});
6
+ try {
7
+ const input = JSON.parse(fs.readFileSync(0, 'utf8'));
8
+ const gate = readUsageGate();
9
+ if (input.hook_event_name === 'PreToolUse') {
10
+ if (gate.state === 'paused' && ['Agent', 'Task'].includes(input.tool_name)) console.log(JSON.stringify({ hookSpecificOutput: {
11
+ hookEventName: 'PreToolUse', permissionDecision: 'deny', permissionDecisionReason: pauseReason(gate),
12
+ } }));
13
+ } else if (['SessionStart', 'UserPromptSubmit'].includes(input.hook_event_name)) {
14
+ if (gate.state === 'paused') console.log(`Claude's usage is past the pause line (${gate.pause_at}%). Running steps finish; start no new work, builders or agents until ${gate.resets_at}; answer the owner's messages briefly with "paused until <that time, in the owner's local time>".`);
15
+ else if (gate.state === 'going') console.log(`Keep going is on: work runs past the pause line until ${gate.resets_at}, spending usage.`);
16
+ }
17
+ } catch {}
18
+ process.exitCode = 0;
@@ -0,0 +1 @@
1
+ var k=Object.defineProperty;var s=(r,t)=>k(r,"name",{value:t,configurable:!0});import{readManifest as b,withManifestLock as g,writeManifest as m,isBoardSite as p,listBoards as A,appendEvent as R}from"./boards.mjs";import{routeRecord as w}from"./routes.mjs";import{repoService as v,validateGithub as S,validateBranch as l,runRepo as x,githubFromOrigin as E}from"./repos.mjs";import{storedPath as f}from"../bin/install.mjs";import{cloneRoot as C}from"../shared/repos.mjs";import{githubFromOrigin as K}from"./repos.mjs";const d=s((r,t=400)=>{throw Object.assign(new Error(r),{status:t})},"refuse");function D(r,t=b()){const e=w(t.sites,r);return p(e)||d("Board not found.",404),e.hub===!0?{hub:!0,github:"",branch:"",repo:f(C())}:{github:e.github||"",branch:e.branch||"",repo:f(e.owner?.repo||"")}}s(D,"boardRepo");function y(r,t){const e=D(r,t);return e.hub&&d("The hub board works on kickflip\u2019s own clone, so its repo can\u2019t change.",409),e}s(y,"changeable");async function P(r,t,{repos:e=v(),event:a=R}={}){y(r),(!t||typeof t!="object"||Array.isArray(t)||Object.keys(t).some(i=>!["github","branch"].includes(i)))&&d("Send only github and branch.");const n=S(t.github),o=l(t.branch),c=f(await e.ensure(n,o));return g(async()=>{const i=b(),h=y(r,i),u=w(i.sites,r);Object.assign(u,{github:n,branch:o,updated:new Date().toISOString()}),u.owner={...u.owner,repo:c},await m(i);const O=A(i).find(j=>j.route===r),B=await a(O,"repo-changed",{github:n,branch:o,repo:c,previous:h});return{github:n,branch:o,repo:c,id:B}})}s(P,"changeBoardRepo");async function M(r,t){const e=await r("git",["-C",t,"symbolic-ref","--quiet","--short","refs/remotes/origin/HEAD"]),a=await r("git",["-C",t,"symbolic-ref","--quiet","--short","HEAD"]);for(const n of[e.status===0&&e.stdout.trim().replace(/^origin\//,""),a.status===0&&a.stdout.trim()])try{if(n)return l(n)}catch{}return""}s(M,"defaultBranch");async function T({run:r=x}={}){return g(async()=>{const t=b(),e=Array.isArray(t.migrated)?t.migrated:[];if(e.includes("board-github")||!Object.keys(t.sites||{}).length)return[];const a=[];for(const[n,o]of Object.entries(t.sites||{})){if(!p(o)||o.github||o.hub)continue;const c=o.owner?.repo||o.dir,i=await r("git",["-C",c,"remote","get-url","origin"]),h=i.status===0?E(i.stdout):null;if(!h)continue;o.github=h;const u=o.branch||await M(r,c);u&&(o.branch=u),a.push(n)}return t.migrated=[...e,"board-github"],await m(t),a})}s(T,"migrateBoardRepos");export{D as boardRepo,P as changeBoardRepo,K as githubFromOrigin,T as migrateBoardRepos};
package/hub/boards.mjs ADDED
@@ -0,0 +1,4 @@
1
+ var z=Object.defineProperty;var o=(t,e)=>z(t,"name",{value:e,configurable:!0});import m from"node:fs";import c from"node:path";import Z from"node:os";import q from"node:crypto";import{execFileSync as Q}from"node:child_process";import{fileURLToPath as X}from"node:url";import{routeRecord as d}from"./routes.mjs";import{deliver as Y,readSettings as tt}from"./push.mjs";import{DEFAULTS as et,windowAt as F}from"./schedule.mjs";import{liveSessions as j}from"./live.mjs";import{withFileLock as nt}from"./locks.mjs";import{dropSessionPrompts as st,readPrompts as rt,savePrompts as ot,withPromptsLock as it}from"./prompts.mjs";import{storedPath as y}from"../bin/install.mjs";import{hubService as at}from"../shared/service.mjs";const ct=c.resolve(c.dirname(X(import.meta.url)),".."),A=c.join(Z.homedir(),".claude"),w=c.join(A,"kickflip"),P=c.join(A,"waves"),b=c.join(w,"manifest.json"),ut=c.join(w,"github-state.json"),lt=c.join(w,"wake-state.json"),C=c.join(w,"deaf-state.json"),E=c.join(w,"resume-state.json"),_=c.join(w,"link-faults.json"),B=c.join(w,"hub-code.json"),ft=c.join(w,"push","subs.json"),M=c.join(A,"slack-bridge","topics"),dt=["merged","dropped"],pt="1970-01-01T00:00:00Z",ht=2*6e4,mt=15*6e4,f=o((t,e)=>{try{return JSON.parse(m.readFileSync(t,"utf8"))}catch{return e}},"readJSON");function k(t,e){const n=`${t}.${process.pid}.tmp`;m.mkdirSync(c.dirname(t),{recursive:!0}),m.writeFileSync(n,JSON.stringify(e,null,2)+`
2
+ `),m.renameSync(n,t)}o(k,"saveJSON");const g=o(()=>f(b,{sites:{}}),"readManifest"),wt=o(t=>nt(b,t),"withManifestLock");function kt(t){return it(()=>{const e=[...Object.values(t.sites||{}),...Object.values(t.closed||{}).map(i=>i.site)];for(const i of e.filter(Boolean))i.dir&&(i.dir=y(i.dir)),Array.isArray(i.cmd)&&(i.cmd=i.cmd.map(u=>y(u))),i.owner?.repo&&(i.owner.repo=y(i.owner.repo));for(const i of Object.values(t.starting||{}))i.repo&&(i.repo=y(i.repo));const n=g(),s=o((i,u)=>d(i.sites,u)?.owner||d(i.starting,u),"owner"),r=new Set([...Object.keys(n.sites||{}),...Object.keys(t.sites||{}),...Object.keys(n.starting||{}),...Object.keys(t.starting||{})]),a=[];for(const i of r){const u=s(n,i),l=s(t,i);(u?.session!==l?.session||u?.since!==l?.since)&&a.push(u?.session,l?.session)}if(a.some(Boolean)){const i=rt();st(i,a),ot(i)}k(b,t)})}o(kt,"writeManifest");const $=o(t=>`${t.repo}#${t.pr}`,"prKey"),T=o((t,e)=>`${t} ${e}${t===1?"":"s"}`,"plural"),H=o(t=>(Array.isArray(t)?t:[]).map(e=>typeof e=="string"?e:e?.name).filter(e=>typeof e=="string"&&e),"names"),U=o(t=>Array.isArray(t?.items),"isWave"),v=o(t=>Array.isArray(t?.cmd)&&t.cmd.some(e=>/board[/\\]server\.mjs$/.test(e))&&typeof t.dir=="string","isBoardSite");function gt(t,e,n){if(n==="file"){const r=f(c.resolve(t,e.items),null);return U(r)?[{slug:e.slug||c.basename(e.items,".json"),wave:r}]:[]}let s=[];try{s=m.readdirSync(P,{withFileTypes:!0}).filter(r=>r.isDirectory()).map(r=>r.name).sort()}catch{}return s.map(r=>({slug:r,wave:f(c.join(P,r,"wave.json"),null)})).filter(r=>U(r.wave))}o(gt,"wavesOf");function jt(t,e){return t.flatMap(({slug:n,wave:s})=>{const r=e||s.repo;return r?s.items.filter(a=>Number.isInteger(a?.pr)&&!dt.includes(a.status)).map(a=>({wave:n,id:a.id,pr:a.pr,repo:r,since:a.gh_since||pt})):[]})}o(jt,"prsOf");function N(t,e){const n=f(c.join(e.dir,"board.json"),{}),s=typeof n.items=="string"&&n.items!=="waves"?"file":"waves",r=gt(e.dir,n,s);return{route:t,dir:e.dir,port:e.port,title:n.title||e.title||t,kind:s,waves:r,prs:jt(r,e.github),github:e.github,branch:e.branch,auto_build:n.auto_build===!0,reviewers:H(n.reviewers),owner:e.owner||null,watch:Array.isArray(e.watch)&&e.watch.length?e.watch:["data/events.jsonl"],filter:e.filter||null}}o(N,"boardOf");function D(t=g()){return Object.entries(t?.sites||{}).filter(([,e])=>v(e)).map(([e,n])=>N(e,n))}o(D,"listBoards");function ne(t=g()){const e=Object.entries(t?.sites||{}).find(([,n])=>n?.hub===!0&&v(n));return e?N(...e):void 0}o(ne,"hubBoard");const J="hub-flag";function se(){return wt(async()=>{const t=f(b,null);if(!t||typeof t.sites!="object"||!t.sites)return null;const e=Array.isArray(t.migrated)?t.migrated:[];if(e.includes(J))return null;const n=d(t.sites,"hub-orchestrator"),s=v(n)&&!Object.values(t.sites).some(r=>r?.hub===!0);return s&&(n.hub=!0),t.migrated=[...e,J],await kt(t),s?"hub-orchestrator":null})}o(se,"migrateHubFlag");function yt(t,e){const n=o(s=>{try{return m.realpathSync(s)}catch{return c.resolve(s)}},"real");return c.resolve(t)===c.resolve(e)||n(t)===n(e)}o(yt,"samePath");function re(t,e=g()){const n=Object.entries(e?.sites||{}).find(([,s])=>v(s)&&yt(s.dir,t));return n?N(...n):null}o(re,"boardAt");async function G(t,e,n){try{const r=await fetch(`http://127.0.0.1:${t.port}/_claude/events`,{method:"POST",headers:{"content-type":"application/json"},body:JSON.stringify({kind:e,...n}),signal:AbortSignal.timeout(5e3)});if(r.ok)return(await r.json()).id}catch{}const s={id:"e"+Date.now().toString(36)+Math.random().toString(36).slice(2,5),ts:new Date().toISOString(),kind:e,data:n};return m.mkdirSync(c.join(t.dir,"data"),{recursive:!0}),m.appendFileSync(c.join(t.dir,"data","events.jsonl"),JSON.stringify(s)+`
3
+ `),s.id}o(G,"appendEvent");async function oe(t,e,n){const s=[],r=[];for(const a of t)try{s.push({route:a.route,id:await G(a,e,n)})}catch(i){r.push(`${a.route}: could not add the ${e} event (${i.message})`)}return{events:s,failed:r}}o(oe,"sendToBoards");function bt(t,e=Date.now()){const n=f(c.join(t,"data","watcher.json"),null);if(!Number.isInteger(n?.pid))return!1;if(typeof n.exited_at=="string"&&e-Date.parse(n.exited_at)<=ht)return!0;try{process.kill(n.pid,0)}catch(s){if(s.code!=="EPERM")return!1}try{return Q("ps",["-p",String(n.pid),"-o","command="],{encoding:"utf8",timeout:3e3}).includes("wait-events.mjs")}catch{return!1}}o(bt,"watcherRunning");function vt(t,{now:e=Date.now(),running:n}={}){const s=t.owner?.session;return!!(s&&(n??j()).has(s))&&bt(t.dir,e)}o(vt,"isListening");function I(t,{now:e=Date.now(),running:n=j(),wakes:s=f(E,{})}={}){if(vt(t,{now:e,running:n}))return"listening";const r=t.owner?.session,a=d(s,t.route);return r&&t.owner.repo&&!n.has(r)&&!a?.error&&!a?.pushed_at?"asleep":"deaf"}o(I,"listenState");const L=o((t,e)=>c.join(t.dir,"data",`.watch-cursor-${e}.json`),"cursorPath"),S=o(t=>L(t,t.owner.session),"cursorFile");function ie(t){const e=t.owner?.session?f(S(t),{}):{};return Object.fromEntries(t.watch.map(n=>{const s=d(e,n);return[n,Number.isSafeInteger(s)&&s>=0?s:0]}))}o(ie,"watcherCursor");const ae=o(t=>Object.fromEntries(Object.entries(R(t)).map(([e,n])=>[e,n.length])),"watchedCounts");function ce(t,{from:e,to:n=e,mark:s=null}){const r=f(L(t,e),null);if(!r&&!s)return;const a=Object.assign(Object.create(null),r);for(const[i,u]of Object.entries(s||{}))a[i]=Math.min(d(a,i)??u,u);k(L(t,n),a)}o(ce,"carryCursor");function R(t){const e=t.filter?new RegExp(t.filter):null;return Object.fromEntries(t.watch.map(n=>{let s=[];try{s=m.readFileSync(c.resolve(t.dir,n),"utf8").split(`
4
+ `).filter(Boolean)}catch{}return[n,e?s.filter(r=>e.test(r)):s]}))}o(R,"watchedLines");function St(t){if(!t.owner?.session)return null;const e=f(S(t),null),n=e?0:Date.parse(t.owner.since||"")||0;let s=null;for(const[r,a]of Object.entries(R(t)))for(const i of e?a.slice(d(e,r)||0):a){let u=null;try{u=JSON.parse(i).ts}catch{}const l=Date.parse(u);Number.isNaN(l)||l<n||(!s||l<Date.parse(s))&&(s=u)}return s}o(St,"oldestUnheard");async function ue({manifest:t,now:e=new Date,running:n=j()}={}){const s=Object.assign(Object.create(null),f(C,{})),r=[],a=[];let i=!1;const u=f(E,{});for(const l of D(t)){const p=I(l,{now:e.getTime(),running:n,wakes:u});if(p==="listening"){d(s,l.route)&&(delete s[l.route],i=!0);continue}if(p==="asleep"||d(s,l.route))continue;const h=St(l);if(!h||e-Date.parse(h)<mt)continue;const x=Math.round((e-Date.parse(h))/6e4),W=n.has(l.owner.session)?"no watcher runs. Relaunch it in the board's session.":"its session isn't running, and the hub can't wake it.";try{await Y({title:`${l.title} isn't listening`,body:`An event has waited ${x} min and ${W}`,url:`/${l.route}/`,tag:`deaf-${l.route}`},{kind:"fault",immediate:!0,at:e}),s[l.route]={at:e.toISOString(),since:h},i=!0,r.push(l.route)}catch(V){a.push(`${l.route}: could not push that it isn't listening (${V.message})`)}}return i&&k(C,s),{pushed:r,failed:a}}o(ue,"checkListening");const Ot=o(t=>c.join(t,"data","handshake.json"),"handshakeFile");async function le(t,e=g()){const n=D(e).find(r=>r.route===t);if(!n)throw new Error(`/${t} is not a board site in the manifest`);n.owner?.session&&!m.existsSync(S(n))&&k(S(n),Object.fromEntries(Object.entries(R(n)).map(([r,a])=>[r,a.length])));const s={id:await G(n,"hello",{title:n.title}),at:new Date().toISOString(),session:n.owner?.session||null};return k(Ot(n.dir),{hello:s,ack:null}),{route:t,hello:s.id}}o(le,"handshake");const fe=["github","events","wakes","push","slack"];function xt(){let t=[];try{t=m.readdirSync(M).filter(e=>e.endsWith(".json"))}catch{}return Object.assign({},...t.map(e=>f(c.join(M,e),{}).items||{}))}o(xt,"topicItems");function At(t,e){const n=e?.lastCheck;if(!n?.at)return{link:"github",ok:!1,detail:"The hub hasn't checked GitHub yet."};const s=t.prs.filter(a=>e.prs?.[$(a)]?.at!==n.at).map(a=>`#${a.pr}`);if(s.length)return{link:"github",ok:!1,detail:`${[...new Set(s)].join(", ")} missed the last check.`};if(!(n.boards||[]).includes(t.route))return{link:"github",ok:!1,detail:"The last check didn't reach this board."};const r=new Set(t.prs.map($)).size;return{link:"github",ok:!0,detail:r?`${T(r,"open PR")}, all in the last check.`:"No open PRs; the last check reached this board."}}o(At,"githubLink");const Et={listening:{ok:!0,detail:"Its session and a watcher are running."},asleep:{ok:!0,detail:"Asleep: its session idled out, and the hub wakes it on the next event."},deaf:{ok:!1,detail:"Not listening: its session or its watcher isn't running, and the hub can't wake it, so events wait."}},_t=o(t=>({link:"events",listen:t,...Et[t]}),"eventsLink");function $t(t,e,n){const s=e?.wakes||[];return s.length?(n?.last?.failed||[]).includes(t.route)?{link:"wakes",ok:!1,detail:`The last wake (${n.last.due.join(", ")}) couldn't reach this board.`}:{link:"wakes",ok:!0,detail:`Daily at ${s.join(", ")}.`}:{link:"wakes",ok:!1,detail:"No daily wakes are set (Hub, Polling)."}}o($t,"wakesLink");const Tt=o(t=>t?{link:"push",ok:!0,detail:`${T(t,"device")} subscribed.`}:{link:"push",ok:!1,detail:"No device has turned on notifications."},"pushLink");function Nt(t,e,n,s,r){const a=e?.review_sent;return!!(d(d(t.review_link_ts,e?.pr),s)||d(d(t.review_link_ts,n),s)||d(e?.review_link_ts,s)||(Array.isArray(a)?a.includes(s):d(a,s))||d(d(r,n)?.review_link_ts,s))}o(Nt,"sentTo");function Dt(t,e,n){const s=[];let r=0;for(const a of t.prs){const i=e?.prs?.[$(a)];if(i?.state!=="OPEN"||i.isDraft)continue;r++;const{wave:u}=t.waves.find(h=>h.slug===a.wave),l=u.items.find(h=>h.id===a.id),p=[...new Set([...t.reviewers,...H(u.reviewers)])].filter(h=>!Nt(u,l,`${a.wave}/${a.id}`,h,n));p.length&&s.push(`#${a.pr} has no link for ${p.join(", ")}`)}return s.length?{link:"slack",ok:!1,detail:`${s.join("; ")}.`}:{link:"slack",ok:!0,detail:r?`Every reviewer has the link to ${T(r,"PR")} in review.`:"No PR is in review."}}o(Dt,"slackLink");const It=/\b(?:from|import)\s*['"](\.\.?\/[^'"]+)['"]/g;function K(t=c.join(ct,"hub","hub.mjs")){const e=c.dirname(c.dirname(t)),n=new Map,s=o(i=>{if(n.has(i))return;let u;try{u=m.readFileSync(i)}catch{return}n.set(i,u);for(const l of u.toString("utf8").matchAll(It))s(c.resolve(c.dirname(i),l[1]))},"visit");s(t);const r=[...n.keys()].map(i=>c.relative(e,i)).sort(),a=q.createHash("sha256");for(const i of r)a.update(i).update("\0").update(n.get(c.join(e,i))).update("\0");return{hash:a.digest("hex").slice(0,12),files:r}}o(K,"hubCode");function de(){const t={...K(),pid:process.pid,at:new Date().toISOString()};return k(B,t),t}o(de,"recordHubCode");const Lt=at().restartCommand,Rt=o(()=>f(B,null)?.hash??null,"runningHubCode"),Ft=o(()=>({running:Rt(),onDisk:K().hash}),"hubCodeState"),pe=o(({running:t,onDisk:e}=Ft())=>t&&t===e?{link:"hub",ok:!0,detail:`The hub runs the code in its files (${e}).`}:{link:"hub",ok:!1,detail:`The hub runs older code than its files; restart it with ${Lt}`},"hubLink"),O=6e4,Pt={github:o(t=>(t+10)*O,"github"),events:o(()=>2*O,"events"),wakes:o(t=>(t+10)*O,"wakes"),push:o(()=>0,"push"),slack:o(t=>(t+10)*O,"slack")};function Ct(t,e,n){const s=n?.windows?.length?n:et;return Math.max(F(new Date(t),s).every_minutes,F(new Date(e),s).every_minutes)}o(Ct,"everyFor");function Bt(t,e,{settings:n,now:s,faults:r}){const a=r===void 0,i=a?d(f(_,{}),t)||{}:r,u={},l=e.map(p=>{if(p.ok)return{...p,state:"ok"};const h=d(i,p.link)||new Date(s).toISOString();u[p.link]=h;const x=Pt[p.link](Ct(Date.parse(h),s,n));return{...p,state:s-Date.parse(h)>=x?"broken":"settling",since:h}});if(a&&JSON.stringify(u)!==JSON.stringify(i)){const p=Object.assign(Object.create(null),f(_,{}));Object.keys(u).length?p[t]=u:delete p[t];try{k(_,p)}catch{}}return l}o(Bt,"settle");function Mt(t,e={}){const n=e.github??f(ut,{}),s=e.settings??tt(),r=[At(t,n),_t(e.listen??I(t)),$t(t,s,e.wakes??f(lt,{})),Tt(e.devices??(f(ft,[])||[]).length),Dt(t,n,e.topics??xt())];return Bt(t.route,r,{settings:s,now:e.now??Date.now(),faults:e.faults})}o(Mt,"linksOf");function he(t){const e={running:j(),wakes:f(E,{})};for(const n of D(t))Mt(n,{listen:I(n,e)})}o(he,"recordLinkFaults");export{mt as DEAF_AFTER,ut as GITHUB_STATE,Pt as GRACE,B as HUB_CODE,fe as LINKS,E as RESUME_STATE,lt as WAKE_STATE,G as appendEvent,re as boardAt,ce as carryCursor,ue as checkListening,le as handshake,Ot as handshakeFile,ne as hubBoard,K as hubCode,Ft as hubCodeState,pe as hubLink,v as isBoardSite,vt as isListening,Mt as linksOf,D as listBoards,I as listenState,se as migrateHubFlag,St as oldestUnheard,$ as prKey,jt as prsOf,f as readJSON,g as readManifest,de as recordHubCode,he as recordLinkFaults,Rt as runningHubCode,yt as samePath,k as saveJSON,oe as sendToBoards,ae as watchedCounts,R as watchedLines,ie as watcherCursor,bt as watcherRunning,wt as withManifestLock,kt as writeManifest};
@@ -0,0 +1 @@
1
+ var S=Object.defineProperty;var c=(s,i)=>S(s,"name",{value:i,configurable:!0});import O from"node:fs";import{carryCursor as k,isBoardSite as m,listBoards as y,readManifest as l,watcherCursor as x,withManifestLock as b,writeManifest as w}from"./boards.mjs";import{routeRecord as d,validRoute as j}from"./routes.mjs";import{stopSession as C,withSessionLock as g}from"./sessions.mjs";import{liveSessions as v}from"./live.mjs";import{closeBoardBuilders as M,openBoardBuilders as N}from"../board/store.mjs";const n=c((s,i=400)=>{throw Object.assign(new Error(s),{status:i})},"refuse"),u=c(s=>{j(s)||n("Choose a board route.")},"checkRoute");function p(s,i){u(s);const o=y(i).find(f=>f.route===s);return o||n("That board is not open.",404),o}c(p,"boardOf");async function L(s,i=l()){const o=p(s,i);return{route:s,title:o.title,hub:d(i.sites,s).hub===!0,prs:o.prs,builders:await N(o.dir)}}c(L,"boardClosure");function A(s=l()){return Object.entries(s.closed||{}).filter(([,i])=>m(i?.site)).map(([i,o])=>({route:i,title:o.title||o.site.title||i,closedAt:o.closed_at,dir:o.site.dir}))}c(A,"closedBoards");async function K(s,i,{live:o,stop:f}={}){return u(s),g(s,()=>b(async()=>{const r=l(),e=p(s,r),t=d(r.sites,s);t.hub===!0&&n("The hub board cannot be closed while it is flagged as the hub.",409),i!==e.title&&n("Type the board\u2019s name exactly to close it."),d(r.closed,s)&&n("That route already has a closed board.",409);const a=t.owner?.session&&y(r).filter(h=>h.route!==s&&h.owner?.session===t.owner.session);a?.length&&n(`This session is also recorded by ${a.map(h=>`${h.title} (/${h.route})`).join(", ")}; close refused.`,409);const T=x(e);await C(t.owner,{live:o,stop:f});const B=await M(e.dir);return r.closed={...r.closed,[s]:{site:t,title:e.title,cursor:T,closed_at:new Date().toISOString()}},delete r.sites[s],await w(r),{route:s,filesKept:t.dir,buildersClosed:B.length,next:"/_hub/"}}))}c(K,"closeBoard");async function q(s,{run:i,live:o}={}){return u(s),g(s,async({resume:f})=>{const r=await b(async()=>{const e=l(),t=d(e.closed,s);return m(t?.site)||n("That board is not closed.",404),(d(e.sites,s)||d(e.starting,s))&&n("That route is already in use.",409),Object.values(e.sites||{}).some(a=>a.port===t.site.port)&&n("The board\u2019s recorded port is in use by another site.",409),O.statSync(t.site.dir,{throwIfNoEntry:!1})?.isDirectory()||n("The board\u2019s folder is missing.",409),(!t.site.owner?.session||!t.site.owner.repo)&&n("The board has no recorded session and folder to resume.",409),e.sites||={},e.sites[s]=t.site,t.cursor&&k(p(s,e),{from:t.site.owner.session,mark:t.cursor}),delete e.closed[s],Object.keys(e.closed).length||delete e.closed,await w(e),t});try{const e=await f({run:i,live:o,mark:r.cursor});return{route:s,session:e.session}}catch(e){const t=d(l().sites,s);if(e.reason==="live"&&t?.owner?.session&&(o||v)().has(t.owner.session))return{route:s,session:t.owner.session};throw await b(async()=>{const a=l();JSON.stringify(d(a.sites,s))===JSON.stringify(r.site)&&(delete a.sites[s],a.closed={...a.closed,[s]:r},await w(a))}),e}})}c(q,"reopenBoard");export{L as boardClosure,K as closeBoard,A as closedBoards,q as reopenBoard};
package/hub/files.mjs ADDED
@@ -0,0 +1 @@
1
+ var g=Object.defineProperty;var m=(e,t)=>g(e,"name",{value:t,configurable:!0});import l from"node:fs/promises";import a from"node:path";const w={".html":"text/html; charset=utf-8",".htm":"text/html; charset=utf-8",".css":"text/css; charset=utf-8",".js":"text/javascript; charset=utf-8",".mjs":"text/javascript; charset=utf-8",".json":"application/json; charset=utf-8",".svg":"image/svg+xml",".png":"image/png",".jpg":"image/jpeg",".jpeg":"image/jpeg",".gif":"image/gif",".webp":"image/webp",".ico":"image/x-icon",".txt":"text/plain; charset=utf-8",".md":"text/markdown; charset=utf-8",".woff2":"font/woff2",".pdf":"application/pdf",".mp4":"video/mp4",".webm":"video/webm"},n=m((e,t,c,s={})=>{e.writeHead(t,{"content-type":"text/plain; charset=utf-8","cache-control":"no-store",...s}),e.end(c)},"plain");async function E(e,t,{dir:c,rest:s,prefix:d,headers:u={}}){if(e.method!=="GET"&&e.method!=="HEAD")return n(t,405,"A page served from its files takes only GET.",{allow:"GET, HEAD"});const i=new URL(s,"http://page");let r;try{r=decodeURIComponent(i.pathname).split("/")}catch{return n(t,400,"Bad path.")}if(r.some(f=>f.startsWith(".")||f.includes("\0")))return n(t,404,"Not found.");const p=a.resolve(c);let o=a.resolve(p,"."+r.join("/"));if(o!==p&&!o.startsWith(p+a.sep))return n(t,404,"Not found.");if((await l.stat(o).catch(()=>null))?.isDirectory()){if(!i.pathname.endsWith("/"))return t.writeHead(301,{location:`${d}${i.pathname}/${i.search}`,"cache-control":"no-store"}),t.end();o=a.join(o,"index.html")}let h;try{h=await l.readFile(o)}catch{return n(t,404,"Not found.")}t.writeHead(200,{"content-type":w[a.extname(o).toLowerCase()]||"application/octet-stream","cache-control":"no-store","content-length":h.length,...u}),t.end(e.method==="HEAD"?void 0:h)}m(E,"serveFiles");export{E as serveFiles};
package/hub/github.mjs ADDED
@@ -0,0 +1,5 @@
1
+ var B=Object.defineProperty;var h=(e,s)=>B(e,"name",{value:s,configurable:!0});import D from"node:fs";import O from"node:path";import J from"node:os";import{execFile as z}from"node:child_process";import{wakesDue as C,localDay as K}from"./schedule.mjs";import{GITHUB_STATE as A,WAKE_STATE as G,listBoards as M,appendEvent as j,prKey as k,readJSON as H,saveJSON as _}from"./boards.mjs";const P=O.join(J.homedir(),".claude","skills","monitor","monitor-gh.sh"),W="/opt/homebrew/bin:/usr/local/bin:/usr/bin:/bin:/usr/sbin:/sbin",E=["ci","review","comments","merged","mergeable","draft"],b=h(e=>e&&e!=="UNKNOWN"?e:null,"known");function F(e){const r=(Array.isArray(e.events)?e.events:[]).at(-1);return{state:e.state||null,isDraft:!!e.isDraft,reviewDecision:e.reviewDecision||null,mergeable:e.mergeable||null,mergeStateStatus:e.mergeStateStatus||null,head:e.head||null,failing:[...new Set(e.failing||[])].sort(),pending:Number(e.pending)||0,eventId:r?String(r.id):null,eventAt:r?.at||null}}h(F,"summarize");function U(e,s){const r=new Set;if(!e)return s.state==="MERGED"&&r.add("merged"),s.eventId&&r.add("comments"),E.filter(i=>r.has(i));(s.pending===0&&(e.pending>0||e.head!==s.head)||e.failing.join(`
2
+ `)!==s.failing.join(`
3
+ `))&&r.add("ci"),e.reviewDecision!==s.reviewDecision&&r.add("review"),s.eventId&&s.eventId!==e.eventId&&r.add("comments"),s.state==="MERGED"&&e.state!=="MERGED"&&r.add("merged");for(const i of["mergeable","mergeStateStatus"])b(e[i])&&b(s[i])&&e[i]!==s[i]&&r.add("mergeable");return e.isDraft!==s.isDraft&&r.add("draft"),E.filter(i=>r.has(i))}h(U,"changesBetween");function R(e,s){const r={...s};for(const n of["mergeable","mergeStateStatus"])!b(s[n])&&b(e?.[n])&&(r[n]=e[n]);return r}h(R,"carry");function L({pr:e,repo:s,since:r}){return new Promise((n,i)=>{z("/bin/zsh",[P,String(e),r],{env:{...process.env,PATH:W,MONITOR_REPO:s},timeout:9e4,maxBuffer:16*1024*1024},(a,f,c)=>{if(a)return i(new Error(`${s}#${e}: ${String(c||a.message).trim().split(`
4
+ `)[0].slice(0,200)}`));try{n(JSON.parse(f))}catch(g){i(new Error(`${s}#${e}: unreadable output (${g.message})`))}})})}h(L,"runMonitorGh");function q(e,s){const r=O.join(e,"data","checked.json"),n=`${r}.${process.pid}.tmp`;D.mkdirSync(O.dirname(r),{recursive:!0}),D.writeFileSync(n,JSON.stringify(s?{at:new Date().toISOString(),every:s}:{at:new Date().toISOString(),every:null,idle:!0},null,2)+`
5
+ `),D.renameSync(n,r)}h(q,"writeChecked");async function te({every:e,manifest:s}){const r=new Date().toISOString(),n=H(A,{prs:{}});n.prs||={},n.acknowledged||={};const i=M(s),a=new Map;for(const t of i.flatMap(l=>l.prs)){const l=a.get(k(t));(!l||t.since<l.since)&&a.set(k(t),t)}const f=[],c={},g={};for(const[t,l]of a){try{c[t]=F(await L(l))}catch(p){c[t]=null,f.push(p.message);continue}g[t]=[]}const u=[],m=[],I=new Set;for(const t of i){const l=t.prs.map(o=>{const d=k(o),w=JSON.stringify([t.route,o.wave,o.id]),S=n.acknowledged[d]?.[w]||n.prs[d],v=c[d]?U(S,c[d]):[];return v.length&&(g[d]=E.filter(T=>g[d].includes(T)||v.includes(T))),{p:o,key:d,recipient:w,previous:S,changes:v}}),p=l.filter(o=>o.changes.length);let $=t.prs.every(o=>c[k(o)]),N=!0;if(p.length)try{u.push({route:t.route,id:await j(t,"github-change",{prs:p.map(({p:o,changes:d})=>({wave:o.wave,id:o.id,pr:o.pr,changes:d}))})})}catch(o){f.push(`${t.route}: could not add the github-change event (${o.message})`);for(const{key:d}of p)I.add(d);$=N=!1}for(const{key:o,recipient:d,previous:w,changes:S}of l)c[o]&&(N||!S.length)&&(n.acknowledged[o]||={},n.acknowledged[o][d]=R(w,c[o]));if($)try{q(t.dir,t.prs.length?e:null),m.push(t.route)}catch(o){f.push(`${t.route}: could not write checked.json (${o.message})`)}}const y=[];for(const[t,l]of Object.entries(c))!l||I.has(t)||(g[t].length&&y.push({repo:a.get(t).repo,pr:a.get(t).pr,changes:g[t]}),n.prs[t]={...R(n.prs[t],l),at:r},delete n.acknowledged[t]);for(const t of Object.keys(n.acknowledged))a.has(t)||delete n.acknowledged[t];return n.lastCheck={at:r,watched:a.size,failed:f.length,changed:y.length,boards:m},_(A,n),{at:r,watched:a.size,failed:f,changes:y,events:u,checked:m}}h(te,"checkGitHub");async function ne({now:e=new Date,settings:s,manifest:r}){const n=H(G,{});n.sent||={};const i=C(e,s,n.sent),a=[],f=[],c=[];if(!i.length)return{due:i,events:a,failed:f};const g=M(r);for(const u of g)try{a.push({route:u.route,id:await j(u,"check-github",{reason:"morning"})})}catch(m){f.push(`${u.route}: could not add the morning check-github event (${m.message})`),c.push(u.route)}if(a.length||!g.length){for(const u of i)n.sent[u]=K(e);n.last={at:e.toISOString(),due:i,routes:a.map(u=>u.route),failed:c},_(G,n)}return{due:i,events:a,failed:f}}h(ne,"sendWakes");export{E as CHANGE_KINDS,R as carry,U as changesBetween,te as checkGitHub,ne as sendWakes,F as summarize};
package/hub/home.mjs ADDED
@@ -0,0 +1 @@
1
+ var c=Object.defineProperty;var u=(e,n)=>c(e,"name",{value:n,configurable:!0});import{siteIcon as g}from"./icons.mjs";const d=u(e=>Number.isInteger(e)&&e>0?e:0,"count");function k(e){const{items:n,working:r,summary:o,lanes:t,building:i}=e||{};if(!Array.isArray(n))return null;const s=[...new Set(n.map(a=>String(a?.kind||"").trim()).filter(Boolean))],l=t&&typeof t=="object"&&["review","progress","todo"].every(a=>t[a]===void 0||Number.isInteger(t[a])&&t[a]>=0);return{count:n.length,working:d(r),summary:typeof o=="string"&&o.trim()?o.trim():n.length?`${n.length} waiting${s.length?`: ${s.join(", ")}`:""}`:"",lanes:l?{review:d(t.review),progress:d(t.progress),todo:d(t.todo)}:null,building:i===!0}}u(k,"readNeeds");function f({state:e,working:n,count:r,building:o,listen:t}={}){return e==="down"?{tone:"down",word:"Down"}:t==="deaf"?{tone:"down",word:"Not listening"}:e==="starting"?{tone:"starting",word:"Starting"}:e==="stopped"?{tone:"starting",word:"Stopped"}:r?{tone:"needs",word:`${r} need you`}:n?{tone:"working",word:"Working"}:t==="asleep"?{tone:"asleep",word:"Asleep"}:o?{tone:"building",word:"Building"}:null}u(f,"siteStatus");function w(e){return e?[["review","review"],["progress","doing"],["todo","todo"]].filter(([n])=>e[n]>0).map(([n,r])=>({lane:n,text:`${e[n]} ${r}`})):null}u(w,"laneChips");const p=u(e=>String(e??"").replace(/[&<>"']/g,n=>({"&":"&amp;","<":"&lt;",">":"&gt;",'"':"&quot;","'":"&#39;"})[n]),"esc");function h(e,n,r){const o=f({state:e,...n,listen:r}),t=e==="up"?w(n?.lanes):null,i=o?`<span class="dot t-${o.tone}${o.tone==="working"?" pulse":""}" aria-hidden="true"></span><span class="word t-${o.tone}">${p(o.word)}</span>`:"",s=t===null?"":t.length?t.map(l=>`<span class="lc lc-${l.lane}">${p(l.text)}</span>`).join(""):"<span>nothing open</span>";return i||s?`${i}${s?`<span class="lcs">${s}</span>`:""}`:""}u(h,"statusLine");function x(e,n,r,o=new Map){return e.map(t=>{const i=n.get(t.route)??null,s=r.has(t.route)?o.get(t.route)??"listening":void 0;return{route:t.route,title:t.title,board:r.has(t.route),state:t.state,count:i?.count||0,changedAt:t.changedAt||0,created:t.created||null,icon:g(t.route,t),sub:h(t.state,i,s)}})}u(x,"sidebarSites");function y(e){return e.filter(n=>n.board).sort((n,r)=>r.count-n.count||r.changedAt-n.changedAt||n.route.localeCompare(r.route))[0]?.route??null}u(y,"startRoute");const $="runs older code than its files; restart it";function A(e,n=new Map,r=null){const o=e.filter(s=>s.state==="down"||(n.get(s.route)||[]).some(l=>l.state==="broken")),t=e.filter(s=>s.state==="up").length,i=r?.ok===!1;return{tone:o.length?"down":i?"wait":"ok",word:o.length?`${o.length} broken`:i?"Needs a restart":"Healthy",detail:`${t} of ${e.length} site${e.length===1?"":"s"} up${i?` \xB7 ${$}`:""}`,broken:o.map(s=>s.route)}}u(A,"hubHealth");export{$ as STALE_CODE,A as hubHealth,w as laneChips,k as readNeeds,x as sidebarSites,f as siteStatus,y as startRoute,h as statusLine};
package/hub/hub.mjs ADDED
@@ -0,0 +1,17 @@
1
+ #!/usr/bin/env node
2
+ var ut=Object.defineProperty;var u=(e,t)=>ut(e,"name",{value:t,configurable:!0});import Se from"node:http";import $e from"node:net";import{spawn as lt,execFileSync as dt}from"node:child_process";import b from"node:fs";import S from"node:path";import xe from"node:os";import{fileURLToPath as ht}from"node:url";import ve from"node:vm";import ft from"node:zlib";import{routeRecord as T}from"./routes.mjs";import{publicKey as pt,addSubscription as mt,sendAll as gt,flushQueue as _e,queueStatus as ee,saveSettings as wt,readSettings as D,migrateSettings as bt,setNotifications as yt,readCodexShare as ke,setCodexShare as St,deliver as $t,pushRoute as xt}from"./push.mjs";import{POLL_MS as Te,answerOf as vt,expired as _t,pendingFor as je,promptRecord as kt,pushOf as Tt,updatePrompts as H,shown as jt,pruneRemembered as te,rememberRules as Ot,rememberedAnswer as Pt,rememberedFor as Oe}from"./prompts.mjs";import{nextSlot as Rt,windowAt as Et,hold as Dt,heldUntil as Ht,holdLine as Lt,editPolling as Ct}from"./schedule.mjs";import{capital as It,hubView as At,settingsView as Mt,pollingView as Bt,flushView as Ft,SNOOZE_REFUSED as Nt}from"./views.mjs";import{checkGitHub as Ut,sendWakes as Gt}from"./github.mjs";import{listBoards as O,isBoardSite as zt,listenState as Jt,RESUME_STATE as Wt,sendToBoards as Pe,checkListening as Kt,recordHubCode as Vt,runningHubCode as Zt,migrateHubFlag as Xt,recordLinkFaults as Qt,linksOf as Yt,hubLink as qt,hubCodeState as en,readJSON as tn}from"./boards.mjs";import{liveSessions as Re}from"./live.mjs";import{RuleError as nn,applyRules as sn,changed as Ee,readHubRules as ne,writeHubRules as an}from"./rules.mjs";import{siteIcon as on}from"./icons.mjs";import{readNeeds as rn,sidebarSites as cn,startRoute as un,hubHealth as ln}from"./home.mjs";import{checkSessions as dn,startingBoards as hn}from"./sessions.mjs";import{repoService as fn,repoLookupRoute as pn}from"./repos.mjs";import{boardRepo as mn,changeBoardRepo as gn,migrateBoardRepos as wn}from"./board-repo.mjs";import{requestBoard as bn,dismissBoard as yn}from"./new-board.mjs";import{boardClosure as Sn,closeBoard as $n,closedBoards as xn,reopenBoard as vn}from"./close-board.mjs";import{crossSite as _n,isJSON as De}from"../starter/cross-site.mjs";import{BOARD_PUBLIC as se,appFile as kn,colorsVersion as He,destOf as Tn,fileVersion as jn,pageOf as Le,shellHtml as On}from"./shell.mjs";import{hubStream as Pn}from"./stream.mjs";import{usageReader as Rn,checkUsagePause as En}from"./usage.mjs";import{readUsageGate as Ce,setPauseAt as Dn,setKeepGoing as Hn}from"../shared/usage-gate.mjs";import{codexStatusReader as Ln}from"../shared/codex.mjs";import{BuildError as Cn,startBuild as In,stopTries as An,tryTick as Mn,servingTry as Ie,initializeTries as Bn,fetchTryBranches as Fn}from"./try.mjs";import{migrateOnStart as Nn}from"../bin/install.mjs";import{serveFiles as Un}from"./files.mjs";import{LinearError as ae,linearStatus as oe,removeLinearKey as Gn,saveLinearKey as zn,savedClient as Ae}from"./linear.mjs";const U=S.dirname(ht(import.meta.url)),Jn=S.join(U,"..","shared","colors.css"),h=u((...e)=>console.log(new Date().toISOString(),...e),"log"),Me=Nn();for(const e of Me.steps)h(`migration: ${e}`);for(const e of Me.conflicts)h(`migration failed: ${e}`);const G=S.join(xe.homedir(),".claude","kickflip"),Be=S.join(G,"manifest.json"),re=S.join(G,"logs"),Fe=S.join(G,"run"),ie=Number(process.env.KICKFLIP_HUB_PORT||4500);for(const e of[G,re,Fe])b.mkdirSync(e,{recursive:!0});try{const e=Vt();h(`hub code ${e.hash} (${e.files.length} files)`)}catch(e){h("could not record the hub code hash:",e.message)}try{const e=await Xt();e&&h(`manifest: flagged /${e} as the hub board`)}catch(e){h("hub board migration failed:",e.message)}try{const e=await wn();e.length&&h(`manifest: recorded GitHub repos for ${e.map(t=>`/${t}`).join(", ")}`)}catch(e){h("board repo migration failed:",e.message)}const L=fn(),Wn=Rn(),z=Ln();let J={version:1,sites:{}};function w(){try{J=JSON.parse(b.readFileSync(Be,"utf8")),J.sites||={}}catch(e){e.code==="ENOENT"?J={version:1,sites:{}}:h("manifest unreadable, keeping the last good copy:",e.message)}return J}u(w,"loadManifest");const x=new Map,R=new Set;let W=!1;const Ne=5e3,ce=u(e=>JSON.stringify([e.cmd,e.cwd||e.dir,e.port,e.env||{},e.restart_at||0]),"keyOf"),ue=u(e=>Array.isArray(e?.cmd)&&e.cmd.length>0&&!!e.port,"runs"),C=u(e=>ue(e)?"cmd":e?.port?"port":"files","servesOf"),I=new Map;function K(e){return S.join(Fe,e+".pid")}u(K,"pidFile");function Kn(e,t){try{const n=Number(b.readFileSync(K(e),"utf8")),o=n?dt("ps",["-o","command=","-p",String(n)],{encoding:"utf8"}).trim():"";o&&o.includes(t.cmd[t.cmd.length-1])&&(process.kill(-n,"SIGTERM"),h(`stopped stale ${e} (pgid ${n})`))}catch{}try{b.unlinkSync(K(e))}catch{}}u(Kn,"killStale");function Vn(e,t){const n=x.get(e)||{restarts:0},o=b.openSync(S.join(re,e+".log"),"a"),[s,...l]=t.cmd,c={...process.env,...t.env||{},PORT:String(t.port),PREFIX:"/"+e,KICKFLIP_ROUTE:e};b.writeSync(o,`
3
+ --- ${new Date().toISOString()} hub starting: ${t.cmd.join(" ")} (port ${t.port})
4
+ `);let f;try{f=lt(s,l,{cwd:t.cwd||t.dir,env:c,stdio:["ignore",o,o],detached:!0})}catch(y){b.writeSync(o,`hub could not start it: ${y.message}
5
+ `),b.closeSync(o),Object.assign(n,{child:null,key:ce(t),state:"crashed",exitCode:null,nextStart:Date.now()+Ue(n.restarts++)}),x.set(e,n);return}b.closeSync(o),b.writeFileSync(K(e),String(f.pid)),Object.assign(n,{child:f,key:ce(t),state:"starting",since:Date.now(),exitCode:null}),x.set(e,n),h(`started ${e} pid ${f.pid}`),f.pid&&R.add(f);const g=u((y,m)=>{if(R.delete(f),n.child!==f)return;try{b.unlinkSync(K(e))}catch{}Date.now()-n.since>6e4&&(n.restarts=0),Object.assign(n,{child:null,state:n.stopping?"stopped":"crashed",exitCode:y??m,nextStart:Date.now()+Ue(n.restarts++)}),n.stopping=!1,h(`${e} exited (${y??m})`)},"ended");f.on("error",y=>{h(`${e} spawn error: ${y.message}`),f.pid||g(y.code,null)}),f.on("exit",g)}u(Vn,"start");const Ue=u(e=>Math.min(6e4,1e3*2**Math.min(e,6)),"backoff");function le(e){const t=x.get(e);if(!t||!t.child){x.delete(e);return}t.stopping=!0;const n=t.child;V(n,"SIGTERM"),setTimeout(()=>{R.has(n)&&V(n,"SIGKILL")},Ne)}u(le,"stop");const V=u((e,t)=>{try{process.kill(-e.pid,t)}catch{}},"signal");function Ge(e){return new Promise(t=>{const n=$e.connect({port:e,host:"127.0.0.1"}),o=u(s=>{n.destroy(),t(s)},"done");n.setTimeout(800,()=>o(!1)),n.on("connect",()=>o(!0)),n.on("error",()=>o(!1))})}u(Ge,"probe");let ze=!0;const A=new Set;async function Je(){if(W)return;const{sites:e}=w();for(const t of[...x.keys()])A.has(t)||(T(e,t)?ue(T(e,t))||(h(`${t} no longer has a command; stopping it`),le(t),x.delete(t)):(h(`${t} left the manifest; stopping it`),le(t),x.delete(t)));for(const t of[...I.keys()])C(T(e,t))!=="port"&&I.delete(t);for(const[t,n]of Object.entries(e))C(n)==="port"&&I.set(t,await Ge(n.port));for(const[t,n]of Object.entries(e)){if(!ue(n))continue;ze&&Kn(t,n);const o=x.get(t);if(o&&o.child&&o.key!==ce(n)){h(`${t} entry changed; restarting it`),le(t),o.restarts=0,o.nextStart=Date.now()+1500;continue}(!o||!o.child&&Date.now()>=(o.nextStart||0))&&Vn(t,n)}ze=!1;for(const[t,n]of x){const o=T(e,t);o&&(n.up=n.child?await Ge(o.port):!1,n.child&&n.up&&(n.state="up"))}}u(Je,"reconcile");const Zn=setInterval(()=>Je().catch(e=>h("reconcile failed:",e.message)),2e3);Je().catch(e=>h("reconcile failed:",e.message));try{bt()&&h("settings: migrated the push schedule to the polling interval")}catch(e){h("settings migration failed:",e.message)}const Xn=6e4,Qn=60*6e4,$={running:null,startedAt:0,firstAt:Date.now()+Xn,last:null,errorLoggedAt:0},We=u(()=>new Date($.startedAt?Rt(new Date($.startedAt),D()).getTime():$.firstAt),"nextPollAt");function Ke({flush:e=!0}={}){if($.running)return $.running;const t=new Date;return $.startedAt=t.getTime(),$.running=(async()=>{const n=D();let o;try{o=await Ut({every:Et(t,n).every_minutes,manifest:w()})}catch(s){o={at:t.toISOString(),watched:0,failed:[s.message],changes:[],events:[],checked:[]}}await Fn({manifest:w(),log:h}),o.failed.length&&Date.now()-$.errorLoggedAt>Qn&&($.errorLoggedAt=Date.now(),h(`github check: ${o.failed.length} step${o.failed.length===1?"":"s"} failed, first: ${o.failed[0]} (logged at most hourly)`)),o.changes.length&&h(`github check: ${o.changes.map(s=>`#${s.pr} ${s.changes.join(",")}`).join("; ")} \u2192 ${o.events.map(s=>s.route).join(", ")||"no board"}`);try{o.deaf=await Kt({manifest:w()})}catch(s){o.deaf={pushed:[],failed:[s.message]}}(o.deaf.pushed.length||o.deaf.failed.length)&&h(`not listening: pushed ${o.deaf.pushed.join(", ")||"none"}${o.deaf.failed.length?`; ${o.deaf.failed.join("; ")}`:""}`);try{Qt(w())}catch(s){h("could not record link faults:",s.message)}if(o.flushed=null,o.held=Ht(t,n)!==null,e&&!o.held)try{const s=await _e();s&&(o.flushed={count:s.count,sent:s.sent,ok:s.ok},h(`push queue: sent ${s.sent} of ${s.count} queued`))}catch(s){h("push queue flush failed:",s.message)}return $.last=o,o})().finally(()=>{$.running=null}),$.running}u(Ke,"runPoll");let Ve=null;function Yn(){Ve||=Gt({settings:D(),manifest:w()}).then(e=>{e.due.length&&h(`wake ${e.due.join(", ")}: check-github \u2192 ${e.events.map(t=>t.route).join(", ")||"no board"}${e.failed.length?`; ${e.failed.join("; ")}`:""}`)}).catch(e=>h("wake failed:",e.message)).finally(()=>{Ve=null})}u(Yn,"runWakes");const Z={running:null};function qn(){Z.running||A.size||(Z.running=dn({manifest:w()}).then(e=>{(e.resumed.length||e.pushed.length||e.failed.length)&&h(`sessions: resumed ${e.resumed.join(", ")||"none"}; pushed ${e.pushed.join(", ")||"none"}${e.failed.length?`; ${e.failed.join("; ")}`:""}`)}).catch(e=>h("session check failed:",e.message)).finally(()=>{Z.running=null}))}u(qn,"runSessionCheck"),Bn(w());const es=Date.now(),M=new Map;function B(e){e?(X.delete(e),v.changed(e)):v.refresh()}u(B,"promptChanged");async function Ze(){const{gone:e,rulesChanged:t}=await H(n=>{const o=_t(n,{now:Date.now(),aliveSince:es}),s=te(n,de());for(const l of o)delete n.prompts[l.id];return{gone:o,rulesChanged:s}});for(const n of e)B(n.route);t&&v.refresh(),e.length&&h(`prompts: dropped ${e.map(n=>n.id).join(", ")}, whose hook went away`)}u(Ze,"prunePrompts");const de=u(()=>({running:Re(),boards:O(w())}),"promptContext");let he=!1;async function Xe(){if(!he){he=!0;try{await En({gate:Ce(),resume:u(async e=>{const t=await Pe(O(w()),"usage-resumed",{window:e});for(const n of t.failed)h(n)},"resume")})}finally{he=!1}}}u(Xe,"checkUsageGate"),setInterval(()=>{Mn({manifest:w()}).catch(e=>h("try tick failed:",e.message)),Xe().catch(e=>h("usage gate failed:",e.message)),!$.running&&Date.now()>=We().getTime()&&Ke().catch(e=>h("poll failed:",e.message)),Yn(),Ze().catch(e=>h("prompt expiry failed:",e.message)).finally(qn)},15e3);function Qe(){if(W)return;W=!0,clearInterval(Zn),h("hub stopping; stopping sites");for(const n of x.values())n.child&&(n.stopping=!0);for(const n of R)V(n,"SIGTERM");let e=!1;An().finally(()=>{e=!0});const t=Date.now()+Ne;setInterval(()=>{if(!((!e||R.size)&&Date.now()<t)){for(const n of R)h(`pid ${n.pid} ignored SIGTERM; killing it`),V(n,"SIGKILL");process.exit(0)}},50)}u(Qe,"shutdown"),process.on("SIGTERM",Qe),process.on("SIGINT",Qe);const Ye=new Map;function qe(e,t={n:3e3}){let n=0,o;try{o=b.readdirSync(e,{withFileTypes:!0})}catch{return 0}for(const s of o){if(t.n--<=0)break;if(s.name.startsWith(".")||s.name==="node_modules")continue;const l=S.join(e,s.name);if(s.isDirectory())n=Math.max(n,qe(l,t));else try{n=Math.max(n,b.statSync(l).mtimeMs)}catch{}}return n}u(qe,"newestMtime");function ts(e,t){const n=Ye.get(e);if(n&&Date.now()-n.checked<3e4)return n.at;const o=qe(t.dir);return Ye.set(e,{at:o,checked:Date.now()}),o}u(ts,"changedAt");const et=u(e=>{try{return typeof e=="string"&&b.statSync(e).isDirectory()}catch{return!1}},"isFolder");function fe(){const{sites:e}=w();return Object.entries(e).sort(([t],[n])=>t.localeCompare(n)).map(([t,n])=>{const o=x.get(t)||{},s=C(n),l=s==="files"?et(n.dir)?"up":"down":s==="port"?I.has(t)?I.get(t)?"up":"down":"starting":o.child?o.up?"up":"starting":o.state==="crashed"?"down":o.state||"starting";return{route:t,title:n.title||t,description:n.description||"",icon:n.icon||null,color:n.color||null,port:n.port??null,serves:s,state:l,exitCode:o.exitCode??null,owner:n.owner||null,changedAt:ts(t,n)||null,created:n.created||null,log:S.join(re,t+".log")}})}u(fe,"status");const j=u(e=>String(e??"").replace(/[&<>"']/g,t=>({"&":"&amp;","<":"&lt;",">":"&gt;",'"':"&quot;","'":"&#39;"})[t]),"esc"),X=new Map,tt=2e4,ns=1500;async function ss(e){try{const t=await fetch(`http://127.0.0.1:${e}/_claude/needs`,{signal:AbortSignal.timeout(ns)});return t.ok?rn(await t.json()):null}catch{return null}}u(ss,"fetchNeeds");async function nt(e,{fresh:t=!1}={}){const n=e.filter(s=>s.state==="up"&&s.owner&&s.port);for(const s of n){const l=X.get(s.route);(t===!0||t?.has?.(s.route)||!l||Date.now()-l.at>=tt)&&X.set(s.route,{at:Date.now(),needs:ss(s.port)})}const o=await Promise.all(n.map(s=>X.get(s.route).needs));return new Map(n.map((s,l)=>[s.route,o[l]]))}u(nt,"waitingCounts");const st=u(e=>[...e.values()].reduce((t,n)=>t+(n?.count||0),0)+je(null).length,"badgeTotal"),F={at:0,value:null};function as(e){if(F.value&&Date.now()-F.at<tt)return F.value;const t=new Map,n=new Map,o={running:Re(),wakes:tn(Wt,{})};for(const s of e)try{const l=Jt(s,o);n.set(s.route,l),t.set(s.route,Yt(s,{listen:l}))}catch(l){h(`links of ${s.route} failed:`,l.message),t.set(s.route,[])}return Object.assign(F,{at:Date.now(),value:{links:t,listens:n}}),F.value}u(as,"boardLinks");async function Q({fresh:e=!1}={}){const t=new Date,n=fe(),o=await nt(n,{fresh:e}),s=w(),l=O(s),{links:c,listens:f}=as(l),g=en(),y=cn(n,o,new Set(l.map(r=>r.route)),f),m=D(),p=Dt(t,m);return{data:{code:g.running??null,total:st(o),start:un(y),sites:y,hub:ln(y,c,qt(g)),home:b.realpathSync(xe.homedir()),starting:hn(s,t.getTime()),usage:Wn({codexShare:ke(z())}),pause:Ce(),notifications:p?p.why==="off"?{state:"off",text:"Off"}:{state:"held",text:It(Lt(p,t))}:{state:"on",text:"On"}},links:c,rows:n,code:g}}u(Q,"sidebarState");function at(e,t,n,o,s){let l=Buffer.isBuffer(s)?s:Buffer.from(s);const c={...o,vary:[o.vary,"accept-encoding"].filter(Boolean).join(", ")};l.length>=1024&&/\bgzip\b/.test(e.headers["accept-encoding"]||"")&&(l=ft.gzipSync(l),c["content-encoding"]="gzip"),c["content-length"]=l.length,t.writeHead(n,c),t.end(e.method==="HEAD"?void 0:l)}u(at,"sendText");async function os(e){const{data:t}=await Q(),n=ve.createContext({});for(const o of["ui-page-header.js","ui-sidebar.js"])ve.runInContext(b.readFileSync(S.join(se,o),"utf8"),n);return On({sidebar:n.BoardUI.sidebar(t,{here:e}),sidebarData:JSON.stringify(t).replace(/</g,"\\u003c"),touchIcon:Y(180)})}u(os,"shellPage");const v=Pn({targets:u(()=>Object.entries(w().sites).filter(([,e])=>zt(e)&&e.port).map(([e,t])=>({route:e,port:t.port})),"targets"),sidebar:u(async({fresh:e})=>(await Q({fresh:e})).data,"sidebar"),log:h});function Y(e){let t="0";try{t=Math.floor(b.statSync(S.join(U,`icon-${e}.png`)).mtimeMs).toString(36)}catch{}return`/_hub/icon-${e}.png?v=${t}`}u(Y,"iconUrl");const rs=u(()=>JSON.stringify({name:"Claude sites",short_name:"Claude",start_url:"/",scope:"/",display:"standalone",background_color:"#121514",theme_color:"#121514",icons:[{src:Y(192),sizes:"192x192",type:"image/png"},{src:Y(512),sizes:"512x512",type:"image/png"}]}),"manifestJson");async function E(e,t=16*1024){let n="";for await(const o of e)if(n+=o,n.length>t)throw Object.assign(new Error("too large"),{status:413});return n}u(E,"readBody");async function k(e,t){if(!De(e.headers))throw Object.assign(new Error("Send the body as application/json."),{status:403});const n=await E(e,t);return n?JSON.parse(n):{}}u(k,"readJSONBody");const pe=u((e,t="")=>`<!doctype html><html lang="en" class="hub-view"><head><meta charset="utf-8"><meta name="viewport" content="width=device-width, initial-scale=1, viewport-fit=cover">
6
+ <title>${j(e)}</title><link rel="manifest" href="/manifest.webmanifest"><link rel="icon" type="image/png" href="/_hub/kickflip.png"><link rel="apple-touch-icon" href="${Y(180)}">
7
+ <meta name="apple-mobile-web-app-title" content="Claude">${t}<link rel="stylesheet" href="/_hub/colors.css?v=${He()}"><link rel="stylesheet" href="/_hub/hub.css"></head><body><main>`,"head"),is=u(e=>/\bapplication\/json\b/.test(e.headers.accept||""),"wantsJSON"),ot=u(()=>({running:!!$.running,nextAt:We(),last:$.last}),"pollState");async function me(e){await Ze();const t=new Date,{data:n,links:o,rows:s,code:l}=await Q();return{...At({now:t,st:ee(t),poll:ot(),sidebar:n,rows:s,links:o,rules:ne(),code:l,host:e.headers["x-forwarded-host"]||e.headers.host||"",codexShare:ke(z()),codex:z(),remembered:new Map(O(w()).map(c=>[c.route,Oe(c.route)]))}),prompts:je(null).map(jt),remembered:Oe(null)}}u(me,"hubData");const ge=u(async()=>{const e=new Date;return{...Mt({now:e,st:ee(e),rules:ne(),route:await xt(),linear:oe()}),closed:xn()}},"settingsData"),rt=u(e=>{const t=new Date;return Bt({now:t,st:ee(t),poll:ot(),select:e})},"pollingData"),P=u((e,t)=>{e.writeHead(303,{location:t,"cache-control":"no-store"}),e.end()},"redirect");async function cs(e,t,n){const o=u((a,r,i,d={})=>{t.writeHead(a,{"content-type":r,"cache-control":"no-store",...d}),t.end(i)},"send"),s=u((a,r)=>{const i=Zt();return o(a,"application/json",JSON.stringify(r),i?{"x-hub-code":i}:{})},"sendJSON"),l=is(e),c=n.pathname,f=/^\/_hub\/close-board\/([a-z0-9-]+)$/.exec(c);if(f&&e.method==="GET")try{return s(200,await Sn(f[1]))}catch(a){return s(a.status||400,{error:a.message})}if(["/_hub/close-board","/_hub/reopen-board"].includes(c)&&e.method==="POST"){let a;try{const r=await k(e);if(a=r.route,c==="/_hub/reopen-board"){const _=await vn(a);return v.refresh(),s(200,_)}if(A.has(a))return s(409,{error:"That board is already closing."});A.add(a);const i=u(()=>A.delete(a),"release");t.once("finish",i),t.once("close",i),await Z.running;const d=await $n(a,r.name);return v.refresh(),s(200,d)}catch(r){return s(r.status||400,{error:r.message})}}if(c==="/_hub/repos"&&e.method==="GET")try{return s(200,await L.list(w(),{fresh:n.searchParams.get("fresh")==="1"}))}catch(a){return s(a.status||502,{error:a.message})}if(c==="/_hub/branches"&&e.method==="GET")try{return s(200,await L.branches(n.searchParams.get("github")))}catch(a){return s(a.status||502,{error:a.message})}if(c==="/_hub/repo"&&e.method==="GET")return pn(n,{repos:L,manifest:w(),sendJSON:s});const g=/^\/_hub\/board-repo\/([a-z0-9-]+)$/.exec(c);if(g&&["GET","POST"].includes(e.method))try{const a=e.method==="GET"?mn(g[1]):await gn(g[1],await k(e),{repos:L});return e.method==="POST"&&v.refresh(),s(200,a)}catch(a){return s(a.status||400,{error:a.message})}if(c==="/_hub/new-board"&&e.method==="POST"){if(!De(e.headers))return o(403,"text/plain; charset=utf-8","Send name, github, branch and purpose as application/json.");let a;try{a=await k(e)}catch{return o(400,"text/plain; charset=utf-8","Send name, github, branch and purpose as JSON.")}try{const{route:r,started:i}=await bn(a,{repos:L,changed:u(()=>v.refresh(),"changed")});return i.then(({error:d})=>{d&&h(`new board /${r} didn't start: ${d}`),v.refresh()}),v.refresh(),s(200,{route:r})}catch(r){if(r.status)return o(r.status,"text/plain; charset=utf-8",r.message);throw r}}const y=/^\/_hub\/new-board\/([a-z0-9-]+)$/.exec(c);if(y&&e.method==="DELETE")try{const a=await yn(y[1]);return v.refresh(),s(200,a)}catch(a){if(a.status)return o(a.status,"text/plain; charset=utf-8",a.message);throw a}if(c==="/_hub/build"&&e.method==="POST"){if(W)return s(503,{error:"The hub is stopping. Try again after it restarts."});const a=["127.0.0.1","::1","::ffff:127.0.0.1"],r=(e.headers.host||"").replace(/:\d+$/,"");if(!a.includes(e.socket.remoteAddress)||!["127.0.0.1","localhost","[::1]"].includes(r)||e.headers.origin||Object.keys(e.headers).some(d=>d==="forwarded"||d.startsWith("x-forwarded-")))return s(403,{error:"Builds can only be started from the host over loopback."});let i;try{i=await k(e)}catch(d){return s(d.status||400,{error:"Send the board route and card key as JSON."})}try{return s(202,await In(i,{manifest:w(),log:h}))}catch(d){if(d instanceof Cn)return s(d.status,{error:d.message});throw d}}if(c==="/sw.js")return o(200,"text/javascript; charset=utf-8",b.readFileSync(S.join(U,"sw.js")),{"service-worker-allowed":"/"});if(c==="/manifest.webmanifest")return o(200,"application/manifest+json",rs());const m=/^\/_hub\/icon-(180|192|512)\.png$/.exec(c);if(m)try{return o(200,"image/png",b.readFileSync(S.join(U,`icon-${m[1]}.png`)),{"cache-control":"max-age=86400"})}catch{t.writeHead(404).end();return}if(c==="/_hub/colors.css"){const a=n.searchParams.get("v")===He()?"public, max-age=31536000, immutable":"no-cache";try{return o(200,"text/css; charset=utf-8",b.readFileSync(Jn),{"cache-control":a})}catch{t.writeHead(404).end();return}}if(c==="/_hub/kickflip.png")try{return o(200,"image/png",b.readFileSync(S.join(se,"kickflip.png")),{"cache-control":"max-age=86400"})}catch{t.writeHead(404).end();return}if(c==="/_hub/hub.css")return o(200,"text/css; charset=utf-8",b.readFileSync(S.join(se,"hub.css")));if(c==="/_hub/sidebar.json")return s(200,(await Q({fresh:n.searchParams.get("fresh")==="1"})).data);if(c==="/_hub/stream"&&e.method==="GET"){v.handle(e,t);return}if(c.startsWith("/_hub/app/")){const a=kn(c.slice(10));if(!a){t.writeHead(404).end();return}const r=S.extname(a),i={".js":"text/javascript; charset=utf-8",".css":"text/css; charset=utf-8",".png":"image/png"}[r],d=n.searchParams.get("v")===jn(a)?"public, max-age=31536000, immutable":"no-cache";return r===".png"?o(200,i,b.readFileSync(a),{"cache-control":d}):at(e,t,200,{"content-type":i,"cache-control":d},b.readFileSync(a))}if(c==="/_hub/api/hub")return s(200,await me(e));if(c==="/_hub/api/settings")return s(200,await ge());if(c==="/_hub/api/polling")return s(200,rt(n.searchParams.get("w")));if(c==="/_hub/push/key")return s(200,{key:pt()});if(c==="/_hub/push/subscribe"&&e.method==="POST"){const a=e.headers["x-forwarded-host"]||e.headers.host;return s(200,{count:mt(await k(e),`https://${a}/`,e.headers["user-agent"])})}if(c==="/_hub/push/test"&&e.method==="POST"){const a=await gt({title:"Claude",body:"Test notification from the hub.",url:"/",tag:"hub-test"});return s(200,{sent:a.filter(r=>r.status>=200&&r.status<300).length,results:a})}if(c==="/_hub/settings/notifications"&&e.method==="POST"){const a=new URLSearchParams(await E(e)),r=a.get("set"),i=r==="on"?{on:a.get("value")==="on"}:r==="weekends"?{weekends_off:a.get("value")==="on"}:a.has("snooze")?{snooze:a.get("snooze")}:null,d="Pass set=on or set=weekends with value=on, or snooze.";if(!i)return l?s(400,{error:d}):o(400,"text/plain; charset=utf-8",d);let _;try{_=await yt(i)}catch(N){if(N.status!==400)throw N;return l?s(400,{error:Nt}):P(t,"/_hub/?refused=snooze#notifications")}return _.dropped&&h(`notifications off: dropped ${_.dropped} queued push${_.dropped===1?"":"es"}`),l?s(200,await ge()):P(t,"/_hub/#notifications")}if(c==="/_hub/settings/codex"&&e.method==="POST"){const a=new URLSearchParams(await E(e));try{St(a.get("value"),z({fresh:!0}))}catch(r){if(r.status!==400&&r.status!==409)throw r;return l?s(r.status,{error:r.message}):o(r.status,"text/plain; charset=utf-8",r.message)}return l?s(200,await me(e)):P(t,"/_hub/")}if(["/_hub/settings/pause-at","/_hub/settings/keep-going"].includes(c)&&e.method==="POST"){const a=new URLSearchParams(await E(e));try{c.endsWith("/pause-at")?Dn(a.get("value")):Hn(a.get("value")),await Xe()}catch(r){if(r.status!==400&&r.status!==409)throw r;return l?s(r.status,{error:r.message}):o(r.status,"text/plain; charset=utf-8",r.message)}return l?s(200,await me(e)):P(t,"/_hub/")}if(c==="/_hub/settings/linear"&&e.method==="POST"){const a=new URLSearchParams(await E(e,4096));try{a.has("remove")?Gn():await zn(a.get("key"))}catch(r){if(!(r instanceof ae))throw r;return l?s(r.status,{error:r.message}):o(r.status,"text/plain; charset=utf-8",r.message)}return h(`linear: key ${a.has("remove")?"removed":"saved"}`),l?s(200,await ge()):P(t,"/_hub/#linear")}if(c==="/_hub/api/linear"&&e.method==="GET"){const a=Ae();if(!a)return s(200,{set:!1});try{const r=await a.teams();return s(200,{...oe(),teams:r.map(({key:i,name:d})=>({key:i,name:d})),projects:r.flatMap(i=>i.projects.map(d=>({id:d.id,name:d.name,team:i.key})))})}catch(r){if(!(r instanceof ae))throw r;return s(200,{...oe(),error:r.message})}}if(c==="/_hub/linear/projects"&&e.method==="POST"){const a=Ae();if(!a)return s(400,{error:"Add a Linear key on Hub first."});let r;try{r=await k(e,4*1024)}catch(d){return s(d.status||400,{error:"Send the team and project name as JSON."})}const i=typeof r.name=="string"?r.name.trim():"";if(!i||i.length>80||typeof r.team!="string"||!/^[A-Z][A-Z0-9]*$/.test(r.team))return s(400,{error:"Name the project and its team."});try{return s(200,await a.createProject(r.team,i))}catch(d){if(!(d instanceof ae))throw d;return s(d.status,{error:d.message})}}if(c==="/_hub/settings/polling"&&e.method==="POST"){const a=new URLSearchParams(await E(e)),r=a.get("op")||"",i=Ct(D(),{op:r,w:a.get("w")||"",value:(a.get("value")||"").trim()});try{if(i.error)throw Object.assign(new Error(i.error),{status:400});wt(i.input)}catch(d){if(d.status!==400)throw d;return l?s(400,{error:d.message}):o(400,"text/plain; charset=utf-8",d.message)}return l?s(200,rt(i.select)):P(t,`/_hub/?w=${encodeURIComponent(i.select)}#polling`)}if(c==="/_hub/settings/rules"&&e.method==="POST"){let a,r;try{a=await k(e,64*1024),r=sn(ne(),a.rules,a.base??[]),Ee(r.changes)&&an(r.rules)}catch(d){if(!(d instanceof nn)&&!(d instanceof SyntaxError))throw d;return s(400,{error:d.message})}if(!Ee(r.changes))return s(200,{rules:r.rules,changed:!1,events:[],failed:[]});const i=await Pe(O(w()),"rules",{scope:"hub",...r.changes});return h(`hub rules saved: rules \u2192 ${i.events.map(d=>d.route).join(", ")||"no board"}${i.failed.length?`; ${i.failed.join("; ")}`:""}`),s(200,{rules:r.rules,changed:!0,...i})}if(c==="/_hub/push/flush"&&e.method==="POST"){const a=await _e();return a&&h(`push queue: ${a.ok?"sent":"could not send"} ${a.count} queued (from the settings page)`),l?s(200,Ft(a)):P(t,`/_hub/settings/polling?flushed=${a?a.ok?a.count:"failed":0}`)}if(c==="/_hub/poll"&&e.method==="POST"){const a=await k(e);return s(200,await Ke({flush:a.flush!==!1}))}if(c==="/_hub/prompts"&&e.method==="POST"){let a;try{a=await k(e,8*1024*1024)}catch(d){if(d.status)throw d;return s(400,{error:"Send the hook input as JSON."})}if(!a?.tool_name||a.hook_event_name&&a.hook_event_name!=="PermissionRequest")return s(400,{error:"Send a PermissionRequest hook input."});const{answer:r,rec:i}=await H(d=>{const _=de(),N=_.boards.find(ye=>ye.owner?.session&&ye.owner.session===a.session_id)||null;te(d,_)&&v.refresh();const be=Pt(d,a);if(be)return{answer:be};const q=kt(a,{board:N,live:_.running.get(a.session_id)});return d.prompts[q.id]=q,{rec:q}});return r?s(200,{state:"answered",answer:r}):(B(i.route),h(`prompt ${i.id}: ${i.route?`/${i.route}`:i.cwd} ${i.tool}${i.action?` (${i.action})`:""}`),$t(Tt(i),{kind:"prompt",immediate:!0}).catch(d=>h(`prompt ${i.id}: push failed: ${d.message}`)),s(200,{id:i.id,deadline:i.deadline}))}const p=/^\/_hub\/prompts\/([\w-]+)(\/wait|\/answer)?$/.exec(c);return p?us(e,t,{id:p[1],op:p[2]?.slice(1)||null,url:n,sendJSON:s}):!1}u(cs,"hubRoute");async function us(e,t,{id:n,op:o,url:s,sendJSON:l}){if(o==="wait"&&e.method==="GET"){const c=u(()=>H(p=>{const a=T(p.prompts,n);return a?.answer?delete p.prompts[n]:a&&(a.seen=new Date().toISOString()),a}),"seen"),f=u(p=>(B(p.route),l(200,{state:"answered",answer:p.answer})),"take"),g=await c();if(!g)return l(404,{state:"gone"});if(g.answer)return f(g);const y=Math.max(0,Math.min(Number(s.searchParams.get("ms"))||Te,Te));if(await new Promise(p=>{const a=M.get(n)||new Set;M.set(n,a);const r=u(()=>{clearTimeout(i),a.delete(r),!a.size&&M.get(n)===a&&M.delete(n),p()},"done"),i=setTimeout(r,y);a.add(r),t.on("close",r)}),t.writableEnded||t.destroyed)return;const m=await c();return m?m.answer?f(m):l(200,{state:"pending"}):l(404,{state:"gone"})}if(o==="answer"&&e.method==="POST"){const c=await k(e),f=await H(m=>{const p=T(m.prompts,n);if(!p)return{status:404,error:"That prompt has expired."};if(p.answer)return{status:409,error:"That prompt was already answered."};const{answer:a,error:r}=vt(p,c);if(r)return{status:400,error:r};p.answer=a;const i=de();return te(m,i),Ot(m,p,i),{rec:p,answer:a}});if(f.error)return l(f.status,{error:f.error});const{rec:g,answer:y}=f;h(`prompt ${n}: ${y.choice}`);for(const m of[...M.get(n)||[]])m();return B(g.route),l(200,{ok:!0})}if(o===null&&e.method==="DELETE"){const c=await H(f=>{const g=T(f.prompts,n);return delete f.prompts[n],g});return c&&(B(c.route),h(`prompt ${n}: its hook gave up`)),l(200,{ok:!0})}return!1}u(us,"promptRoute");const ls=u(({icon:e="",title:t,sub:n=""})=>`<div class="row">${e}<span class="body"><span class="text"><span class="title">${t}</span>${n?`<span class="sub">${n}</span>`:""}</span></span></div>`,"row");function it(e,t){const n=x.get(e)||{},o=n.exitCode!==void 0&&n.exitCode!==null?` (last exit: ${n.exitCode})`:"",s=C(t),[l,c]=s==="port"?["down",`down \xB7 nothing answers on port ${t.port}`]:s==="files"?["down","down \xB7 its folder is missing"]:n.child?["wait","started \xB7 waiting for it to answer"]:["down",`down \xB7 restarting${o}`],f=s==="cmd"?"The hub runs every site and restarts one that stops.":s==="port"?"Its session runs this server, so it comes back when that session starts it again.":"The hub serves this page from a folder that is no longer there.";return`${pe(`${t.title||e} is down`,'<meta http-equiv="refresh" content="5">')}
8
+ <a class="back" href="/_hub/">\u2039 Hub</a>
9
+ <h1>${j(t.title||e)} isn't answering</h1>
10
+ <p class="lede">${j(f)}</p>
11
+ <h2 class="label">Site</h2>
12
+ <div class="group">${ls({icon:on(e,t),title:j(t.title||e),sub:`<span class="st ${l}">${j(c)}</span>`})}</div>
13
+ <p class="note">This page retries every 5 seconds.</p>
14
+ </main></body></html>`}u(it,"downPage");const we=u(e=>new URL(e.url.replace(/^\/+/,"/"),"http://hub"),"requestURL"),ct=Se.createServer((e,t)=>{const n=we(e);if(e.method!=="GET"&&e.method!=="HEAD"&&_n(e.headers)){t.writeHead(403,{"content-type":"text/plain; charset=utf-8","cache-control":"no-store"}),t.end("Requests must come from the app\u2019s own pages.");return}const o=/^\/([^/]+)\/try\/([^/]+)(\/|$)/.exec(n.pathname);let s=null;if(o&&O(w()).some(r=>r.route===o[1])){if(!o[3]){t.writeHead(302,{location:n.pathname+"/"+n.search}).end();return}if(s=Ie(o[1],o[2],{manifest:w(),visit:!0}),!s){t.writeHead(404,{"content-type":"text/html; charset=utf-8","cache-control":"no-store"}),t.end(`${pe("Not being served")}<a class="back" href="/${j(o[1])}/">\u2039 Board</a><h1>${j(o[2].toUpperCase())} isn't being served. Tap Serve on its card.</h1></main></body></html>`);return}}const l=new Set(Object.keys(w().sites));if(!s&&Le({method:e.method,pathname:n.pathname,search:n.search,dest:Tn(e.headers),sites:l})==="shell"){const r=n.pathname==="/"?"":n.pathname.startsWith("/_hub/")?n.pathname.slice(1):decodeURIComponent(n.pathname.split("/")[1]);os(r).then(i=>at(e,t,200,{"content-type":"text/html; charset=utf-8","cache-control":"no-cache",vary:"sec-fetch-dest","x-app-shell":"1"},i)).catch(i=>{t.headersSent||t.writeHead(500,{"content-type":"text/plain"}),t.end(String(i.message||i))});return}if(n.pathname==="/_hub"){t.writeHead(302,{location:"/_hub/"+n.search}).end();return}if(n.pathname==="/_hub/status.json"){t.writeHead(200,{"content-type":"application/json","cache-control":"no-store"}),t.end(JSON.stringify({hub:{port:ie,pid:process.pid},sites:fe()},null,2));return}if(n.pathname==="/_hub/needs.json"){const r=fe();nt(r,{fresh:n.searchParams.get("fresh")==="1"}).then(i=>{t.writeHead(200,{"content-type":"application/json","cache-control":"no-store"}),t.end(JSON.stringify({total:st(i),sites:Object.fromEntries(r.map(d=>[d.route,i.get(d.route)??null]))}))}).catch(i=>{t.headersSent||t.writeHead(500,{"content-type":"text/plain"}),t.end(String(i.message||i))});return}if(n.pathname==="/favicon.ico"){t.writeHead(301,{location:"/_hub/kickflip.png"}).end();return}if(n.pathname==="/sw.js"||n.pathname==="/manifest.webmanifest"||n.pathname.startsWith("/_hub/")){cs(e,t,n).then(r=>{r===!1&&t.writeHead(404).end()}).catch(r=>{t.headersSent||t.writeHead(r.status||500,{"content-type":"text/plain"}),t.end(String(r.message||r))});return}const c=n.pathname.split("/")[1],f=T(w().sites,c);if(!f){t.writeHead(404,{"content-type":"text/html; charset=utf-8"}),t.end(`${pe("No Site")}<a class="back" href="/_hub/">\u2039 Hub</a><h1>No site at /${j(c)}</h1></main></body></html>`);return}if(n.pathname==="/"+c){t.writeHead(302,{location:"/"+c+"/"+n.search}).end();return}const g=s?`/${c}/try/${o[2]}`:"/"+c,y=e.url.slice(g.length)||"/",m=Le({pathname:n.pathname,dest:"document",sites:l})==="shell";if(!s&&C(f)==="files"){if(!et(f.dir)){t.writeHead(502,{"content-type":"text/html; charset=utf-8","cache-control":"no-store"}),t.end(it(c,f));return}Un(e,t,{dir:f.dir,rest:y,prefix:g,headers:m?{vary:"sec-fetch-dest"}:{}}).catch(r=>{t.headersSent||t.writeHead(500,{"content-type":"text/plain"}),t.end(String(r.message||r))});return}const p={...e.headers,"x-forwarded-prefix":g,"x-forwarded-host":e.headers["x-forwarded-host"]||e.headers.host,"x-forwarded-proto":e.headers["x-forwarded-proto"]||"https"},a=Se.request({host:"127.0.0.1",port:s?.port||f.port,method:e.method,path:y,headers:p},r=>{const i={...r.headers};m&&(i.vary=[i.vary,"sec-fetch-dest"].filter(Boolean).join(", ")),typeof i.location=="string"&&i.location.startsWith("/")&&!i.location.startsWith("//")&&!i.location.startsWith(g+"/")&&i.location!==g&&(i.location=g+i.location),t.writeHead(r.statusCode,i),r.pipe(t),r.on("close",()=>{r.complete||t.destroy()})});t.on("close",()=>{t.writableFinished||a.destroy()}),a.on("error",()=>{if(t.headersSent){t.destroy();return}t.writeHead(502,{"content-type":"text/html; charset=utf-8","cache-control":"no-store"}),t.end(it(c,f))}),e.pipe(a)});ct.on("upgrade",(e,t,n)=>{const o=we(e).pathname.split("/")[1],s=T(w().sites,o),l=/^\/[^/]+\/try\/([^/]+)\//.exec(we(e).pathname),c=l&&O(w()).some(p=>p.route===o),f=c?Ie(o,l[1],{manifest:w(),visit:!0}):s;if(!f?.port){t.destroy();return}const g=c?`/${o}/try/${l[1]}`:`/${o}`,y=e.url.slice(g.length)||"/",m=$e.connect(f.port,"127.0.0.1",()=>{const p=[`${e.method} ${y} HTTP/${e.httpVersion}`];for(let a=0;a<e.rawHeaders.length;a+=2)e.rawHeaders[a].toLowerCase()!=="x-forwarded-prefix"&&p.push(`${e.rawHeaders[a]}: ${e.rawHeaders[a+1]}`);p.push(`X-Forwarded-Prefix: ${g}`),m.write(p.join(`\r
15
+ `)+`\r
16
+ \r
17
+ `),n&&n.length&&m.write(n),m.pipe(t),t.pipe(m)});m.on("error",()=>t.destroy()),t.on("error",()=>m.destroy()),t.on("close",()=>m.destroy()),m.on("close",()=>t.destroy())}),ct.listen(ie,"127.0.0.1",()=>h(`hub on http://127.0.0.1:${ie}/ (manifest ${Be})`));
Binary file
Binary file
Binary file
package/hub/icons.mjs ADDED
@@ -0,0 +1 @@
1
+ var p=Object.defineProperty;var h=(t,a)=>p(t,"name",{value:a,configurable:!0});const M=/^#([0-9a-f]{3}|[0-9a-f]{6})$/i,o={phone:'<rect x="6" y="2" width="12" height="20" rx="2.5"/><path d="M11 18h2"/>',audit:'<circle cx="11" cy="11" r="7"/><path d="m20 20-3.5-3.5"/><path d="m8.5 11 1.8 1.8 3.4-3.6"/>',magnifier:'<circle cx="11" cy="11" r="7"/><path d="m20 20-3.5-3.5"/>',clipboard:'<rect x="8" y="2" width="8" height="4" rx="1"/><path d="M16 4h2a2 2 0 0 1 2 2v14a2 2 0 0 1-2 2H6a2 2 0 0 1-2-2V6a2 2 0 0 1 2-2h2"/>',book:'<path d="M2 5h6a4 4 0 0 1 4 4v11a3 3 0 0 0-3-3H2z"/><path d="M22 5h-6a4 4 0 0 0-4 4v11a3 3 0 0 1 3-3h7z"/>',cap:'<path d="M21.42 10.92a1 1 0 0 0-.02-1.84L12.83 5.18a2 2 0 0 0-1.66 0L2.6 9.08a1 1 0 0 0 0 1.83l8.57 3.91a2 2 0 0 0 1.66 0z"/><path d="M22 10v6"/><path d="M6 12.5V16a6 3 0 0 0 12 0v-3.5"/>',chart:'<path d="M3 3v16a2 2 0 0 0 2 2h16"/><path d="M18 17V9"/><path d="M13 17V5"/><path d="M8 17v-3"/>',code:'<path d="m16 18 6-6-6-6"/><path d="m8 6-6 6 6 6"/>',terminal:'<path d="m4 17 6-6-6-6"/><path d="M12 19h8"/>',chat:'<path d="M21 15a2 2 0 0 1-2 2H7l-4 4V5a2 2 0 0 1 2-2h14a2 2 0 0 1 2 2z"/>',calendar:'<rect x="3" y="4" width="18" height="18" rx="2"/><path d="M16 2v4M8 2v4M3 10h18"/>',map:'<path d="M14.1 5.55a2 2 0 0 0 1.8 0l3.65-1.83A1 1 0 0 1 21 4.62v12.76a1 1 0 0 1-.55.9l-4.56 2.27a2 2 0 0 1-1.78 0l-4.22-2.1a2 2 0 0 0-1.78 0l-3.66 1.83A1 1 0 0 1 3 19.38V6.62a1 1 0 0 1 .55-.9l4.56-2.27a2 2 0 0 1 1.78 0z"/><path d="M15 5.76v15"/><path d="M9 3.24v15"/>',wrench:'<path d="M14.7 6.3a1 1 0 0 0 0 1.4l1.6 1.6a1 1 0 0 0 1.4 0l3.77-3.77a6 6 0 0 1-7.94 7.94l-6.91 6.91a2.12 2.12 0 0 1-3-3l6.91-6.91a6 6 0 0 1 7.94-7.94l-3.76 3.76z"/>',server:'<rect x="3" y="3" width="18" height="7" rx="2"/><rect x="3" y="14" width="18" height="7" rx="2"/><path d="M7 6.5h.01M7 17.5h.01"/>',car:'<path d="M19 17h2a1 1 0 0 0 1-1v-3a2 2 0 0 0-1.5-1.9C18.7 10.6 16 10 16 10l-2.2-2.3A2.5 2.5 0 0 0 12 7H5a1.6 1.6 0 0 0-1.4.9l-1.4 2.9A3.7 3.7 0 0 0 2 12v4a1 1 0 0 0 1 1h2"/><circle cx="7" cy="17" r="2"/><path d="M9 17h6"/><circle cx="17" cy="17" r="2"/>',flask:'<path d="M9 3h6"/><path d="M10 3v6.5L4.5 19a1.5 1.5 0 0 0 1.3 2h12.4a1.5 1.5 0 0 0 1.3-2L14 9.5V3"/><path d="M7 15h10"/>',bell:'<path d="M6 8a6 6 0 0 1 12 0c0 7 3 9 3 9H3s3-2 3-9"/><path d="M10.3 21a1.94 1.94 0 0 0 3.4 0"/>',list:'<path d="M9 6h12M9 12h12M9 18h12"/><path d="M4 6h.01M4 12h.01M4 18h.01"/>',globe:'<circle cx="12" cy="12" r="10"/><path d="M2 12h20"/><path d="M12 2a15.3 15.3 0 0 1 4 10 15.3 15.3 0 0 1-4 10 15.3 15.3 0 0 1-4-10 15.3 15.3 0 0 1 4-10z"/>'},y=Object.keys(o),v=[["phone",["ios","iphone","phone","mobile","android"]],["audit",["audit","review","inspect"]],["magnifier",["search","research","find","explore","investigate"]],["book",["learn","course","lesson","lessons","docs","book","guide","teach","study"]],["cap",["school","class","training","tutor"]],["chart",["metrics","dashboard","stats","analytics","chart","report","reports"]],["chat",["chat","ask","questions","qa","talk","voice"]],["calendar",["calendar","schedule","agenda"]],["map",["map","maps","routing","trip","travel"]],["flask",["prototype","prototypes","experiment","lab"]],["terminal",["cli","shell","terminal","script","scripts"]],["code",["code","pr","prs","build","dev","wave","waves"]],["server",["backend","server","api","db","database","sync"]],["car",["car","cars","vehicle","vehicles","inventory"]],["wrench",["tools","tool","fix","ops","setup"]],["bell",["alerts","notifications","notify"]],["list",["todo","todos","tasks","checklist","board"]]];function x(t,a={}){if(Object.hasOwn(o,a.icon||""))return a.icon;const c=new Set(`${t} ${a.title||""}`.toLowerCase().split(/[^a-z0-9]+/));return v.find(([,r])=>r.some(e=>c.has(e)))?.[0]||null}h(x,"iconKey");const s=["#2563eb","#7c3aed","#db2777","#ea580c","#059669","#0891b2","#4f46e5","#b45309"],d=h(t=>String(t??"").replace(/[&<>"']/g,a=>({"&":"&amp;","<":"&lt;",">":"&gt;",'"':"&quot;","'":"&#39;"})[a]),"esc");function b(t,a={}){const c=[...t].reduce((l,n)=>l*31+n.codePointAt(0)>>>0,0),r=M.test(a.color||"")?a.color:s[c%s.length],e=x(t,a),i=e?`<svg width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round">${o[e]}</svg>`:d([...a.title||t][0].toUpperCase());return`<span class="icon${e?"":" letter"}" style="--c:${d(r)}" aria-hidden="true"${e?` data-icon="${e}"`:""}>${i}</span>`}h(b,"siteIcon");export{o as ICONS,y as ICON_KEYS,x as iconKey,b as siteIcon};
package/hub/linear.mjs ADDED
@@ -0,0 +1,2 @@
1
+ var v=Object.defineProperty;var o=(s,u)=>v(s,"name",{value:u,configurable:!0});import y from"node:fs";import h from"node:path";import L from"node:os";class d extends Error{static{o(this,"LinearError")}constructor(u,r=502){super(u),this.status=r}}const f=o(()=>h.join(L.homedir(),".claude","kickflip","linear.json"),"keyFile"),S=o(()=>process.env.KICKFLIP_LINEAR_URL||"https://api.linear.app/graphql","endpoint");function k(){try{const s=JSON.parse(y.readFileSync(f(),"utf8"));return typeof s?.key=="string"&&s.key?s:null}catch{return null}}o(k,"readLinearKey");function g(){const s=k();return s?{set:!0,workspace:s.workspace,url_key:s.url_key}:{set:!1}}o(g,"linearStatus");function $(s,{fetch:u=globalThis.fetch}={}){async function r(t,e={}){let n;try{n=await u(S(),{method:"POST",headers:{"content-type":"application/json",authorization:s},body:JSON.stringify({query:t,variables:e}),signal:AbortSignal.timeout(15e3)})}catch(c){throw new d(`Could not reach Linear: ${c.message}`)}const a=await n.json().catch(()=>null);if(n.status===401||a?.errors?.some(c=>c.extensions?.type==="authentication error"))throw new d("Linear refused the key.",400);if(a?.errors?.length){const c=a.errors[0];throw Object.assign(new d(`Linear: ${c.message}`),{notFound:c.extensions?.type==="not found"||/not found/i.test(c.message)})}if(!n.ok||!a?.data)throw new d(`Linear answered ${n.status}.`);return a.data}o(r,"call");const m="id key name states { nodes { id name type position } } projects(first: 100) { nodes { id name } }";async function p(t){return(await r(`query($key: String!) { teams(filter: { key: { eq: $key } }) { nodes { ${m} } } }`,{key:t})).teams.nodes[0]||(()=>{throw new d(`Linear has no team ${t}.`,400)})()}o(p,"team");const i=o((t,e)=>t.projects.nodes.find(n=>n.id===e)||t.projects.nodes.find(n=>n.name.toLowerCase()===String(e).toLowerCase())||(()=>{throw new d(`Linear's ${t.key} team has no project ${e}.`,400)})(),"projectOf"),l={async viewer(){const t=await r("query { viewer { id name } organization { name urlKey } }");return{viewer:{id:t.viewer.id,name:t.viewer.name},workspace:t.organization.name,url_key:t.organization.urlKey}},async teams(){return(await r("query { teams(first: 100) { nodes { id key name projects(first: 100) { nodes { id name } } } } }")).teams.nodes.map(e=>({id:e.id,key:e.key,name:e.name,projects:e.projects.nodes.map(n=>({id:n.id,name:n.name}))}))},async createProject(t,e){const n=await p(t),a=await r("mutation($input: ProjectCreateInput!) { projectCreate(input: $input) { success project { id name } } }",{input:{name:e,teamIds:[n.id]}});if(!a.projectCreate.success)throw new d("Linear did not create the project.");return{...a.projectCreate.project,team:n.key}},async createIssue({team:t,project:e,title:n,description:a}){const c=await p(t),j={teamId:c.id,projectId:i(c,e).id,title:n,...a?{description:a}:{}},w=await r("mutation($input: IssueCreateInput!) { issueCreate(input: $input) { success issue { identifier url } } }",{input:j});if(!w.issueCreate.success)throw new d("Linear did not create the issue.");return w.issueCreate.issue},async issue(t){try{const e=await r("query($id: String!) { issue(id: $id) { identifier team { key } assignee { id } state { id name type } } }",{id:t});return e.issue?{identifier:e.issue.identifier,team:e.issue.team.key,assignee:e.issue.assignee?.id??null,state:e.issue.state}:null}catch(e){if(e.notFound)return null;throw e}},async moveIssue(t,e){const n=await l.issue(t);if(!n)throw new d(`Linear has no issue ${t}.`,400);if(["completed","canceled"].includes(n.state.type))return null;const a=I((await p(n.team)).states.nodes,e);if(!a||a.id===n.state.id)return null;if(!(await r("mutation($id: String!, $stateId: String!) { issueUpdate(id: $id, input: { stateId: $stateId }) { success } }",{id:t,stateId:a.id})).issueUpdate.success)throw new d("Linear did not move the issue.");return a.name}};return l}o($,"linearClient");function I(s,u){const r=[...s].sort((i,l)=>i.position-l.position),m=o(i=>r.filter(l=>l.type===i),"of"),p=o(i=>/review/i.test(i.name),"review");return u==="started"?m("started").find(i=>/^in progress$/i.test(i.name))||m("started").find(i=>!p(i))||null:u==="review"?m("started").find(p)||null:u==="done"&&(m("completed").find(i=>/^done$/i.test(i.name))||m("completed")[0])||null}o(I,"stateFor");async function O(s,{fetch:u}={}){const r=typeof s=="string"?s.trim():"";if(!r||/\s/.test(r)||r.length>200)throw new d("Paste a Linear personal API key.",400);const m=await $(r,{fetch:u}).viewer(),p=f();y.mkdirSync(h.dirname(p),{recursive:!0});const i=`${p}.${process.pid}.tmp`;return y.writeFileSync(i,JSON.stringify({key:r,...m,saved_at:new Date().toISOString()},null,2)+`
2
+ `,{mode:384}),y.chmodSync(i,384),y.renameSync(i,p),g()}o(O,"saveLinearKey");function _(){return y.rmSync(f(),{force:!0}),g()}o(_,"removeLinearKey");function q({fetch:s}={}){const u=k();return u?{...$(u.key,{fetch:s}),saved:u}:null}o(q,"savedClient");export{d as LinearError,$ as linearClient,g as linearStatus,k as readLinearKey,_ as removeLinearKey,O as saveLinearKey,q as savedClient,I as stateFor};
package/hub/live.mjs ADDED
@@ -0,0 +1,2 @@
1
+ var m=Object.defineProperty;var s=(t,n)=>m(t,"name",{value:n,configurable:!0});import p from"node:fs";import c from"node:path";import d from"node:os";import{execFileSync as l}from"node:child_process";const a=c.join(d.homedir(),".claude","sessions"),f=2e3,o=s(t=>Date.parse(`${String(t).trim().replace(/\s+/g," ")} UTC`),"utc");function S(t,n){return t.filter(e=>{if(!n.has(e.pid))return!1;if(typeof e.procStart!="string")return!0;const r=o(e.procStart);return Number.isNaN(r)||Math.abs(r-n.get(e.pid))<=f})}s(S,"liveOf");function g(t=a){let n=[];try{n=p.readdirSync(t).filter(e=>e.endsWith(".json"))}catch{}return n.flatMap(e=>{try{const r=JSON.parse(p.readFileSync(c.join(t,e),"utf8"));return Number.isInteger(r?.pid)&&typeof r.sessionId=="string"?[r]:[]}catch{return[]}})}s(g,"sessionRecords");function u(t){const n=new Map;if(!t.length)return n;let e="";try{e=l("ps",["-o","pid=,lstart=","-p",t.join(",")],{encoding:"utf8",timeout:3e3,env:{...process.env,TZ:"UTC"}})}catch(r){e=r.stdout||""}for(const r of e.split(`
2
+ `)){const i=r.trim().match(/^(\d+)\s+(.+)$/);i&&n.set(Number(i[1]),o(i[2]))}return n}s(u,"processStarts");function I(t,n=u){if(!Number.isInteger(t?.pid)||t.pid<=0||!t.sessionId||typeof t.procStart!="string")return!1;const e=o(t.procStart);if(!Number.isFinite(e))return!1;const r=n([t.pid]).get(t.pid);return Number.isFinite(r)&&Math.abs(e-r)<=f}s(I,"verifiedProcess");function v({dir:t=a,starts:n=u}={}){const e=g(t);return new Map(S(e,n([...new Set(e.map(r=>r.pid))])).map(r=>[r.sessionId,r]))}s(v,"liveSessions");export{a as SESSIONS_DIR,S as liveOf,v as liveSessions,u as processStarts,g as sessionRecords,I as verifiedProcess};
package/hub/locks.mjs ADDED
@@ -0,0 +1 @@
1
+ var d=Object.defineProperty;var a=(r,c)=>d(r,"name",{value:c,configurable:!0});import t from"node:fs";import o from"node:path";async function u(r,c,{staleAfter:i=3e4,timeout:s=1e4,heartbeat:m=!1}={}){const e=`${r}.lock`;t.mkdirSync(o.dirname(r),{recursive:!0});const y=Date.now()+s;for(;;)try{t.mkdirSync(e);break}catch(n){if(n.code!=="EEXIST")throw n;try{if(Date.now()-t.statSync(e).mtimeMs>i){t.rmdirSync(e);continue}}catch{}if(Date.now()>y)throw Object.assign(new Error(`${o.basename(r)} is locked by another kickflip; try again`),{code:"ELOCKED"});await new Promise(w=>setTimeout(w,100))}const l=m?setInterval(()=>{try{const n=new Date;t.utimesSync(e,n,n)}catch{}},5e3):null;try{return await c()}finally{clearInterval(l);try{t.rmdirSync(e)}catch{}}}a(u,"withFileLock");export{u as withFileLock};
@@ -0,0 +1,2 @@
1
+ var B=Object.defineProperty;var i=(e,t)=>B(e,"name",{value:t,configurable:!0});import{ticketSource as R}from"../board/tickets.mjs";import{readManifest as g,withManifestLock as w,writeManifest as u}from"./boards.mjs";import{ROUTE_RE as v,routeFromName as D,routeRecord as h}from"./routes.mjs";import{repoService as E,validateGithub as I,validateBranch as P}from"./repos.mjs";import{STARTING_WAIT as x,startSession as F}from"./sessions.mjs";import{savedClient as L}from"./linear.mjs";const c=i((e,t=400)=>{throw Object.assign(new Error(e),{status:t})},"refuse");function M(e){(!e||typeof e!="object"||Array.isArray(e)||Object.keys(e).some(n=>!["name","github","branch","purpose","tickets"].includes(n)))&&c("Send only name, github, branch, purpose and tickets."),(typeof e.name!="string"||!e.name.trim()||e.name.length>60)&&c("Name must be 1\u201360 characters."),e.purpose!==void 0&&(typeof e.purpose!="string"||e.purpose.length>240)&&c("Purpose must be at most 240 characters.");const t=e.name.trim().replace(/\s+/g," "),r=D(t);v.test(r)||c("Name must contain a letter or digit for its route.");const a=I(e.github),m=P(e.branch);let d=e.tickets===void 0?{source:"internal"}:e.tickets,o;if(d?.source==="linear"&&typeof d=="object"&&Object.hasOwn(d,"new_project")){const{new_project:n,...b}=d;o=typeof n=="string"?n.trim():"",(!o||o.length>80)&&c("Name the new Linear project."),d={...b,project:o}}return{route:r,title:t,github:a,branch:m,tickets:R(d),newProject:o,purpose:(e.purpose||"").trim().replace(/\s+/g," ")}}i(M,"details");const S=i((e,t)=>Object.hasOwn(e.sites||{},t)||Object.hasOwn(e.starting||{},t)||Object.hasOwn(e.closed||{},t),"taken"),A="That board route already exists. Reopen it from Closed boards if it is closed.",N=i(e=>`'${e.replaceAll("'","'\\''")}'`,"quote"),q=i(({title:e,purpose:t})=>`--template board --title ${N(e)}${t?` --description ${N(t)}`:""}`,"joinFlags");function T(e){const t=String(e).split(`
2
+ `).map(r=>r.trim()).find(Boolean)||"claude did not start";return t.length>200?`${t.slice(0,199)}\u2026`:t}i(T,"errorLine");async function _(e,t){await w(async()=>{const r=g(),a=h(r.starting,e);!a||h(r.sites,e)||(r.starting[e]=t.phase?{...a,phase:t.phase}:t.error?{...a,error:t.error}:{...a,session:t.session,label:t.name,since:new Date().toISOString(),repo:t.repo,...t.model?{model:t.model}:{}},await u(r))})}i(_,"recordStart");async function J(e,{run:t,live:r,repos:a=E(),changed:m=i(()=>{},"changed"),linear:d}={}){const o=M(e),{route:n,title:b,github:f,branch:y,newProject:j}=o;let{tickets:p}=o;const k=a.path(f),O=j===void 0?null:d||L();if(O){S(g(),n)&&c(A);try{p={...p,project:(await O.createProject(p.team,j)).id}}catch(s){c(s.message,s.status||502)}}await w(async()=>{const s=g();S(s,n)&&c(A),s.starting={...s.starting,[n]:{title:b,repo:k,github:f,branch:y,tickets:p,phase:a.cloned(f)?"starting":"cloning",asked_at:new Date().toISOString()}},await u(s)});const $=(async()=>{let s;try{await a.ensure(f,y),await _(n,{phase:"starting"}),m();const l=await F(n,{repo:k,prompt:q(o),run:t,live:r});s={session:l.session,name:l.name,repo:l.repo,model:l.model}}catch(l){s={error:T(l.message)}}try{await _(n,s)}catch(l){return{error:T(`could not record the start: ${l.message}`)}}return s.error?{error:s.error}:{session:s.session}})();return{route:n,started:$}}i(J,"requestBoard");async function Q(e){return w(async()=>{const t=g(),r=h(t.starting,e),a=Date.parse(r?.asked_at||r?.since),m=!!r?.error||Number.isFinite(a)&&Date.now()-a>=x;return(!r||h(t.sites,e)||!m)&&c("Only a board that didn\u2019t start can be dismissed."),delete t.starting[e],Object.keys(t.starting).length||delete t.starting,await u(t),{route:e}})}i(Q,"dismissBoard");export{Q as dismissBoard,J as requestBoard};
@@ -0,0 +1,3 @@
1
+ #!/usr/bin/env node
2
+ var R=Object.defineProperty;var i=(t,a)=>R(t,"name",{value:a,configurable:!0});import D from"node:fs";const p=`http://127.0.0.1:${Number(process.env.KICKFLIP_HUB_PORT||4500)}`,f=10*6e4,O=f+3e4,d=2e3,r="The hub's permission hook failed; ask this as an open question on your board's card instead.",o=i(t=>({hookSpecificOutput:{hookEventName:"PermissionRequest",decision:{behavior:"deny",message:t}}}),"deny");let y=!1;function e(t){y||(y=!0,D.writeSync(1,JSON.stringify(t)+`
3
+ `),process.exit(0))}i(e,"finish"),process.on("uncaughtException",()=>e(o(r))),process.on("unhandledRejection",()=>e(o(r))),setTimeout(()=>e(o(r)),O).unref();const b=i(t=>new Promise(a=>setTimeout(a,t)),"sleep");async function P(){let t="";for await(const n of process.stdin)t+=n;const a=JSON.parse(t),{hookOutput:l,NO_ANSWER:_,UNREACHABLE:A,DROPPED:S,POLL_MS:E}=await import("./prompts.mjs");let h,w;try{const n=await fetch(`${p}/_hub/prompts`,{method:"POST",headers:{"content-type":"application/json"},body:t,signal:AbortSignal.timeout(5e3)});if(!n.ok)throw new Error(`hub ${n.status}`);const s=await n.json();if(s?.state==="answered")return a.tool_name!=="AskUserQuestion"&&s.answer?.choice==="allow"?e(l(a,s.answer)):e(o(r));({id:h,deadline:w}=s)}catch{return e(o(A))}const c=Math.min(Date.parse(w)||1/0,Date.now()+f);for(let n=c-Date.now();n>0;n=c-Date.now()){const s=Math.min(E,n);let u;try{u=await fetch(`${p}/_hub/prompts/${encodeURIComponent(h)}/wait?ms=${s}`,{signal:AbortSignal.timeout(s+1e4)})}catch{await b(Math.min(d,Math.max(0,c-Date.now())));continue}if(u.status===404)return e(o(S));let m=null;try{m=await u.json()}catch{}if(m?.state==="answered")return e(l(a,m.answer));u.ok||await b(Math.min(d,Math.max(0,c-Date.now())))}try{await fetch(`${p}/_hub/prompts/${encodeURIComponent(h)}`,{method:"DELETE",signal:AbortSignal.timeout(2e3)})}catch{}return e(o(_))}i(P,"main"),P().catch(()=>e(o(r)));
@@ -0,0 +1 @@
1
+ var l=Object.defineProperty;var s=(r,e)=>l(r,"name",{value:e,configurable:!0});import{execFile as d}from"node:child_process";const m=3*6e4,i=s(r=>new Promise((e,o)=>{d("ioreg",r,{timeout:5e3,maxBuffer:32*1024*1024},(t,n)=>t?o(t):e(n))}),"ioreg");async function u(r=i){try{const[e,o]=await Promise.all([r(["-n","Root","-d1"]),r(["-c","IOHIDSystem"])]),t=/"CGSSessionScreenIsLocked"\s*=\s*Yes/.test(e),n=[...o.matchAll(/"HIDIdleTime"\s*=\s*(\d+)/g)].map(c=>Number(c[1])/1e6);if(!n.length)return{atMac:!1,error:"ioreg reported no HIDIdleTime"};const a=Math.round(Math.min(...n));return{atMac:!t&&a<m,locked:t,idleMs:a}}catch(e){return{atMac:!1,error:String(e.message||e)}}}s(u,"readPresence");function I({run:r=i,ttl:e=5e3,clock:o=Date.now}={}){let t=null;return()=>((!t||o()-t.at>=e)&&(t={at:o(),reading:u(r)}),t.reading)}s(I,"presenceReader");const p=I();export{m as IDLE_LIMIT_MS,p as presence,I as presenceReader,u as readPresence};
@@ -0,0 +1,2 @@
1
+ var j=Object.defineProperty;var s=(e,t)=>j(e,"name",{value:t,configurable:!0});import S from"node:fs";import y from"node:os";import _ from"node:path";import E from"node:crypto";import{withFileLock as D}from"./locks.mjs";import{routeRecord as l}from"./routes.mjs";const f=_.join(y.homedir(),".claude","kickflip","prompts.json"),v=10*6e4,z=25e3,O=6e4,V="No answer from the owner; ask this as an open question on your board's card instead.",X="The hub can't be reached, so the owner can't answer this prompt; ask this as an open question on your board's card instead.",Y="The hub dropped this prompt; ask this as an open question on your board's card instead.",k="The owner denied this from the hub.",F="The owner skipped this question; ask it as an open question on your board's card instead.",m="AskUserQuestion",d={Edit:"edit",MultiEdit:"edit",Write:"write",Read:"read",NotebookEdit:"edit"},h=s((e,t)=>{const r=String(e??"").replace(/\s+/g," ").trim();return r.length>t?`${r.slice(0,t-1)}\u2026`:r},"cut"),w=s(e=>e?.file_path||e?.notebook_path||"","fileOf"),N=s(e=>{try{return new URL(e).host}catch{return String(e||"")}},"hostOf");function W(e,t={}){if(e==="Bash")return`run ${h(t.command,60)}`;if(l(d,e))return`${l(d,e)} ${_.basename(w(t))||"a file"}`;if(e==="WebFetch")return`fetch ${N(t.url)}`;if(e==="WebSearch")return`search the web for "${h(t.query,50)}"`;const r=/^mcp__(.+?)__(.+)$/.exec(e||"");return r?`use ${r[1]} ${r[2]}`:`use ${e}`}s(W,"actionOf");function T(e,t={}){if(e==="Bash")return String(t.command??"");if(l(d,e))return w(t);if(e==="WebFetch")return String(t.url??"");if(e==="WebSearch")return String(t.query??"");const r=JSON.stringify(t);return r==="{}"?"":h(r,600)}s(T,"detailOf");function P(e){const t=(Array.isArray(e?.permission_suggestions)?e.permission_suggestions:[]).filter(n=>n?.type==="addRules"&&n.behavior==="allow"&&Array.isArray(n.rules)&&n.rules.length).map(n=>({type:"addRules",rules:n.rules,behavior:"allow",destination:"session"}));if(t.length)return t;const r=e?.tool_name,o=e?.tool_input||{},i=r==="Bash"&&o.command?o.command:l(d,r)&&w(o).startsWith("/")?`/${w(o)}`:r==="WebFetch"&&o.url?`domain:${N(o.url)}`:null;return[{type:"addRules",rules:[i?{toolName:r,ruleContent:i}:{toolName:r}],behavior:"allow",destination:"session"}]}s(P,"alwaysRules");const R=s(e=>e.flatMap(t=>t.rules).map(t=>t.ruleContent?`${t.toolName}(${t.ruleContent})`:t.toolName).join(", "),"ruleText"),b=s(e=>Number.isInteger(e?.pid)&&e.pid>0&&typeof e.procStart=="string"&&Number.isFinite(Date.parse(`${e.procStart.trim().replace(/\s+/g," ")} UTC`))?{pid:e.pid,procStart:e.procStart}:null,"runtimeOf"),$=s((e,t)=>!!(b(e)&&b(t)&&e.pid===t.pid&&e.procStart===t.procStart),"sameRuntime"),p=s(e=>typeof e?.owner?.since=="string"&&e.owner.since?e.owner.since:null,"ownerSince"),x=s((e,t,r)=>!!(t?.owner?.session===r&&e.ownerSince&&e.ownerSince===p(t)),"sameOwner");function B(e,t){const r=t?.tool_name;if(r===m||!e||e.toolName!==r||typeof r!="string")return!1;const o=e.ruleContent;if(o===void 0)return!0;if(typeof o!="string"||!o)return!1;const i=t.tool_input||{};if(r==="Bash")return typeof i.command=="string"&&o===i.command;if(l(d,r))return o.startsWith("//")&&!/[\*?\[\]{}]/.test(o)&&o.slice(1)===w(i);if(r==="WebFetch")try{const n=new URL(i.url);return["http:","https:"].includes(n.protocol)&&o===`domain:${n.host}`}catch{return!1}return!1}s(B,"matchesRule");function Z(e,{running:t,boards:r}){let o=!1;for(const[i,n]of Object.entries(e.remembered||{})){const a=r.find(u=>n.route?u.route===n.route:u.owner?.session===i);!$(n.runtime,b(t.get(i)))||n.route&&!x(n,a,i)||a&&!p(a)?(delete e.remembered[i],o=!0):n.route===null&&a&&(n.route=a.route,n.ownerSince=p(a),o=!0)}return o}s(Z,"pruneRemembered");function ee(e,t,r){if(t.answer?.choice!=="always"||t.tool===m||!t.session||!t.permissions||!$(t.runtime,b(r.running.get(t.session))))return!1;const o=l(e.remembered,t.session),i=t.route||o?.route,n=i&&r.boards.find(c=>c.route===i)||!t.route&&r.boards.find(c=>c.owner?.session===t.session);if(t.route&&!x(t,n,t.session)||n&&(n.owner?.session!==t.session||!p(n)))return!1;e.remembered||={};const u=o&&$(o.runtime,t.runtime)&&(o.route?o.route===n?.route&&x(o,n,t.session):!0)?[...o.rules]:[];for(const c of t.permissions.flatMap(g=>g.rules))u.some(g=>g.toolName===c.toolName&&g.ruleContent===c.ruleContent)||u.push(c);return Object.defineProperty(e.remembered,t.session,{value:{route:n?.route||null,ownerSince:p(n),runtime:t.runtime,rules:u},enumerable:!0,writable:!0,configurable:!0}),!0}s(ee,"rememberRules");function te(e,t){return!t.session_id||t.tool_name===m||!l(e.remembered,t.session_id)?null:l(e.remembered,t.session_id).rules.some(r=>B(r,t))?{choice:"allow"}:null}s(te,"rememberedAnswer");function re(e,t=q()){return Object.entries(t.remembered||{}).filter(([,r])=>r.route===e).map(([r,o])=>({session:r,rules:o.rules.map(i=>R([{rules:[i]}]))}))}s(re,"rememberedFor");function oe(e,t){return{hookSpecificOutput:{hookEventName:"PermissionRequest",decision:t?.choice==="deny"?{behavior:"deny",message:t.message||k}:t?.choice==="always"&&e?.tool_name!==m?{behavior:"allow",updatedPermissions:P(e)}:e?.tool_name===m?{behavior:"allow",updatedInput:{...e.tool_input,answers:t?.answers||{}}}:{behavior:"allow"}}}}s(oe,"hookOutput");const A=s(e=>e&&e.startsWith(y.homedir()+"/")?`~${e.slice(y.homedir().length)}`:e||"","tilde");function C(e){return(Array.isArray(e?.questions)?e.questions:[]).filter(r=>typeof r?.question=="string").map(r=>({question:r.question,header:typeof r.header=="string"?r.header:"",multiSelect:r.multiSelect===!0,options:(Array.isArray(r.options)?r.options:[]).filter(o=>typeof o?.label=="string").map(o=>({label:o.label,description:typeof o.description=="string"?o.description:""}))}))}s(C,"questionsOf");function ne(e,{board:t,live:r,now:o=new Date}){const i=String(e.tool_name),n=i===m,a=n?null:P(e),u=o.toISOString();return{id:"p"+E.randomBytes(6).toString("hex"),session:String(e.session_id||""),route:t?.route||null,title:t?.title||A(e.cwd)||"A background session",cwd:String(e.cwd||""),tool:i,action:n?null:W(i,e.tool_input),detail:n?"":T(i,e.tool_input),rule:n?null:R(a),questions:n?C(e.tool_input):null,permissions:a,runtime:b(r),ownerSince:p(t),at:u,deadline:new Date(o.getTime()+v).toISOString(),seen:u,answer:null}}s(ne,"promptRecord");function se(e,t,r=new Date){const o=r.toISOString();if(e.questions){if(t?.choice==="deny")return{answer:{choice:"deny",message:F,at:o}};const i=t?.answers&&typeof t.answers=="object"?t.answers:{};for(const n of e.questions){const a=String(l(i,n.question)??"").split(n.multiSelect?", ":"\0").filter(Boolean);if(!a.length)return{error:`Pick an answer to "${n.question}".`};if(!a.every(u=>n.options.some(c=>c.label===u)))return{error:`"${l(i,n.question)}" isn't an option of "${n.question}".`}}return{answer:{choice:"allow",answers:Object.fromEntries(e.questions.map(n=>[n.question,String(l(i,n.question))])),at:o}}}return["allow","always","deny"].includes(t?.choice)?{answer:{choice:t.choice,...t.choice==="deny"?{message:k}:{},at:o}}:{error:"Pass choice allow, always or deny."}}s(se,"answerOf");function ie(e,{now:t=Date.now(),aliveSince:r=0}={}){return Object.values(e.prompts||{}).filter(o=>t-Date.parse(o.deadline)>O?!0:o.answer?t-Date.parse(o.answer.at)>O:t-Math.max(Date.parse(o.seen),r)>O)}s(ie,"expired");function q(){try{const e=JSON.parse(S.readFileSync(f,"utf8"));return{prompts:e?.prompts&&typeof e.prompts=="object"?e.prompts:{},remembered:e?.remembered&&typeof e.remembered=="object"?e.remembered:{}}}catch{return{prompts:{},remembered:{}}}}s(q,"readPrompts");function I(e){const t=`${f}.${process.pid}.tmp`;S.mkdirSync(_.dirname(f),{recursive:!0}),S.writeFileSync(t,JSON.stringify(e,null,2)+`
2
+ `),S.renameSync(t,f)}s(I,"savePrompts");const L=s(e=>D(f,e),"withPromptsLock");function ae(e){return L(()=>{const t=q(),r=e(t);return I(t),r})}s(ae,"updatePrompts");function ue(e,t){const r=new Set(t.filter(Boolean));for(const o of r)delete e.remembered[o];for(const[o,i]of Object.entries(e.prompts))r.has(i.session)&&delete e.prompts[o]}s(ue,"dropSessionPrompts");function ce(e,t=q()){return Object.values(t.prompts).filter(r=>!r.answer&&r.route===e).sort((r,o)=>r.at.localeCompare(o.at))}s(ce,"pendingFor");const le=s(({id:e,title:t,cwd:r,tool:o,action:i,detail:n,rule:a,questions:u,at:c})=>({id:e,title:t,cwd:A(r),tool:o,action:i,detail:n,rule:a,questions:u,at:c}),"shown");function me(e){const t=e.questions?.[0];return{title:t?`${e.title} asks: ${h(t.question,90)}`:`${e.title} wants to ${e.action}`,body:t?t.options.map(r=>r.label).join(" \xB7 ")+(e.questions.length>1?` (+${e.questions.length-1} more)`:""):h(e.detail,160)||`Answer in ${e.route?"Needs you":"the hub page"}.`,url:e.route?`/${e.route}/`:"/_hub/",tag:`prompt-${e.id}`}}s(me,"pushOf");export{k as DENIED,Y as DROPPED,O as GONE_AFTER,V as NO_ANSWER,z as POLL_MS,F as SKIPPED,X as UNREACHABLE,v as WAIT_MS,W as actionOf,P as alwaysRules,se as answerOf,ue as dropSessionPrompts,ie as expired,oe as hookOutput,B as matchesRule,ce as pendingFor,ne as promptRecord,Z as pruneRemembered,me as pushOf,q as readPrompts,ee as rememberRules,te as rememberedAnswer,re as rememberedFor,I as savePrompts,le as shown,ae as updatePrompts,L as withPromptsLock};
package/hub/push.mjs ADDED
@@ -0,0 +1,2 @@
1
+ var V=Object.defineProperty;var i=(t,e)=>V(t,"name",{value:e,configurable:!0});import m from"node:crypto";import c from"node:fs";import S from"node:path";import v from"node:os";import{validate as X,pollingOf as K,flushAt as G,hold as _,windowAt as Y,notifications as Z,snoozeEnd as tt}from"./schedule.mjs";import{presence as q}from"./presence.mjs";import{CODEX_SHARE_VALUES as et,codexStatus as C,effectiveCodexShare as nt}from"../shared/codex.mjs";const b=S.join(v.homedir(),".claude","kickflip","push"),z=S.join(b,"vapid.json"),F=S.join(b,"subs.json"),h=S.join(b,"queue.json"),A=h+".lock",J=i(t=>S.join(t,".claude","kickflip","settings.json"),"settingsFile"),P=i(t=>Buffer.from(t).toString("base64url"),"b64u"),$=i(t=>Buffer.from(t,"base64url"),"unb64u"),E=i((t,e)=>m.createHmac("sha256",t).update(e).digest(),"hmac"),l=i((t,e)=>{c.mkdirSync(b,{recursive:!0,mode:448});const n=`${t}.${process.pid}.tmp`;c.writeFileSync(n,JSON.stringify(e,null,2),{mode:384}),c.renameSync(n,t)},"writePrivate");function T(){try{return JSON.parse(c.readFileSync(z,"utf8"))}catch{}const t=m.generateKeyPairSync("ec",{namedCurve:"prime256v1"}).privateKey.export({format:"jwk"});return l(z,t),t}i(T,"vapidKey");function ot(){const t=T();return P(Buffer.concat([Buffer.from([4]),$(t.x),$(t.y)]))}i(ot,"publicKey");function g(){try{return JSON.parse(c.readFileSync(F,"utf8"))}catch{return[]}}i(g,"readSubs");function St(t,e,n=""){if(!/^https:\/\//.test(t?.endpoint||"")||!t.keys?.p256dh||!t.keys?.auth)throw Object.assign(new Error("bad subscription"),{status:400});const r=g().filter(s=>s.endpoint!==t.endpoint);return r.push({endpoint:t.endpoint,keys:{p256dh:t.keys.p256dh,auth:t.keys.auth},origin:e,agent:String(n).slice(0,200),added:new Date().toISOString()}),l(F,r),r.length}i(St,"addSubscription");function kt(){return g().map(t=>({host:new URL(t.endpoint).host,agent:t.agent,added:t.added}))}i(kt,"subscriptions");const R=i(t=>!t.agent||/iPhone|iPad|Android/.test(t.agent),"onPhone");async function L(t,e=!1){const n=g();if(t!=="action"||e||!(await q()).atMac)return n;const r=n.filter(s=>!R(s));return r.length?r:n}i(L,"reach");async function xt(){return{atMac:(await q()).atMac,desktops:g().filter(t=>!R(t)).length}}i(xt,"pushRoute");function rt(t,e){const n=$(t.keys.p256dh),r=m.createECDH("prime256v1"),s=r.generateKeys(),o=E($(t.keys.auth),r.computeSecret(n)),a=E(o,Buffer.concat([Buffer.from("WebPush: info\0"),n,s,Buffer.from([1])])),d=m.randomBytes(16),f=E(d,a),w=E(f,Buffer.from("Content-Encoding: aes128gcm\0")).subarray(0,16),k=E(f,Buffer.from("Content-Encoding: nonce\0")).subarray(0,12),x=m.createCipheriv("aes-128-gcm",w,k),O=Buffer.concat([x.update(Buffer.concat([Buffer.from(e),Buffer.from([2])])),x.final(),x.getAuthTag()]),u=Buffer.alloc(4);return u.writeUInt32BE(4096),Buffer.concat([d,u,Buffer.from([s.length]),s,O])}i(rt,"encrypt");function st(t){const e=T(),n=P(JSON.stringify({typ:"JWT",alg:"ES256"})),r=P(JSON.stringify({aud:new URL(t.endpoint).origin,exp:Math.floor(Date.now()/1e3)+12*3600,sub:t.origin||"mailto:kickflip@localhost"})),s=m.sign("sha256",Buffer.from(`${n}.${r}`),{key:m.createPrivateKey({key:e,format:"jwk"}),dsaEncoding:"ieee-p1363"});return`vapid t=${n}.${r}.${P(s)}, k=${ot()}`}i(st,"authorization");async function it(){try{const t=await fetch(`http://127.0.0.1:${Number(process.env.KICKFLIP_HUB_PORT||4500)}/_hub/needs.json?fresh=1`,{signal:AbortSignal.timeout(4e3)}),{total:e}=await t.json();return Number.isInteger(e)?e:void 0}catch{return}}i(it,"badgeTotal");const bt=i(t=>N(g(),t),"sendAll");async function N(t,e){if(!t.length)return[];const n=JSON.stringify({...e,badge:await it()}),r=await Promise.all(t.map(async o=>{try{const a=await fetch(o.endpoint,{method:"POST",headers:{authorization:st(o),"content-encoding":"aes128gcm","content-type":"application/octet-stream",ttl:"86400",urgency:"high"},body:rt(o,n),signal:AbortSignal.timeout(15e3)});return{sub:o,status:a.status,error:a.ok?void 0:(await a.text()).slice(0,300)}}catch(a){return{sub:o,status:0,error:String(a.message||a)}}})),s=new Set(r.filter(o=>o.status===404||o.status===410).map(o=>o.sub.endpoint));return s.size&&l(F,g().filter(o=>!s.has(o.endpoint))),r.map(o=>({host:new URL(o.sub.endpoint).host,agent:o.sub.agent,status:o.status,error:o.error,dropped:s.has(o.sub.endpoint)||void 0}))}i(N,"sendTo");const Q=i(t=>t.some(e=>e.status>=200&&e.status<300),"delivered");function j(t=v.homedir()){try{const e=JSON.parse(c.readFileSync(J(t),"utf8"));return e&&typeof e=="object"&&!Array.isArray(e)?e:{}}catch{return{}}}i(j,"readSettingsFile");function U(t,e=v.homedir()){const n=J(e);c.mkdirSync(S.dirname(n),{recursive:!0});const r=`${n}.${process.pid}.tmp`;c.writeFileSync(r,JSON.stringify(t,null,2)+`
2
+ `),c.renameSync(r,n)}i(U,"writeSettingsFile");const Et=i((t=C(),e=v.homedir())=>nt(j(e).codexShare,t),"readCodexShare");function jt(t,e=C()){if(!et.includes(t))throw Object.assign(new Error("codex-share takes auto, always, or off"),{status:400});if(!e.usable)throw Object.assign(new Error(`${e.words} Run ${e.fix}, then choose again.`),{status:409});return U({...j(),codexShare:t}),t}i(jt,"setCodexShare");function D(){return K(j())}i(D,"readSettings");function Ot(){const t=j();if(!t.push)return null;const{push:e,...n}=t,{notifications:r,...s}=K(t);return U({...n,polling:s}),s}i(Ot,"migrateSettings");function at(t){const{settings:e,error:n}=X({notifications:D().notifications,...t});if(n)throw Object.assign(new Error(n),{status:400});const{push:r,...s}=j(),{notifications:o,...a}=e;return U({...s,polling:a,notifications:o}),e}i(at,"saveSettings");async function Bt(t,e=new Date){const n=D(),r={...n.notifications};if("on"in t&&(r.on=t.on),"weekends_off"in t&&(r.weekends_off=t.weekends_off),"snooze"in t){const a=t.snooze==="off"?null:tt(e,t.snooze,n);if(a===null&&t.snooze!=="off")throw Object.assign(new Error("A snooze must end after now and within 30 days: <n>m, <n>h, <n>d, <n>w, tonight, morning, monday or a date and time; or off."),{status:400});r.snoozed_until=a&&a.toISOString()}const{notifications:s}=at({...n,notifications:r}),o=s.on?0:await y(()=>{const{items:a}=p();return a.length&&l(h,{items:[]}),a.length});return{notifications:s,dropped:o}}i(Bt,"setNotifications");function p(){try{const t=JSON.parse(c.readFileSync(h,"utf8"));return{items:Array.isArray(t.items)?t.items:[]}}catch{return{items:[]}}}i(p,"readQueue");async function y(t){c.mkdirSync(b,{recursive:!0,mode:448});const e=Date.now()+1e4;for(;;)try{c.mkdirSync(A);break}catch(n){if(n.code!=="EEXIST")throw n;try{if(Date.now()-c.statSync(A).mtimeMs>3e4){c.rmdirSync(A);continue}}catch{}if(Date.now()>e)throw new Error("push queue is locked; try again");await new Promise(r=>setTimeout(r,50))}try{return t()}finally{try{c.rmdirSync(A)}catch{}}}i(y,"withQueueLock");function ct(t=new Date){const e=D(),{items:n}=p(),r=_(t,e);return{settings:e,window:Y(t,e),held:r!==null,hold:r,count:n.length,nextFlushAt:n.length?G(t,e):null}}i(ct,"queueStatus");const M=["action","prompt","fault"];class ut extends Error{static{i(this,"PushKindError")}}const ft=i(t=>M.includes(t)?null:`a push needs --kind ${M.join("|")}${t===void 0||t===!0?"":`, not "${t}"`}; news goes on the board, not in a push`,"pushKindError");async function vt(t,{kind:e,urgent:n=!1,immediate:r=!1,at:s=new Date}={}){const o=ft(e);if(o)throw new ut(o);if(!g().length)return{noDevice:!0};const a={title:t.title,body:t.body||"",url:t.url||"/",tag:t.tag||void 0},d=_(s,D());if(d?.why==="off")return{dropped:!0};const f=d!==null;let w=!1;if(n||r&&!f){const u=await N(await L(e,n),a);if(Q(u))return a.tag&&await y(()=>l(h,{items:p().items.filter(B=>B.tag!==a.tag)})),{sent:u};if(n)return{sent:u};w=!0}const k={id:m.randomUUID(),...a,kind:e,queuedAt:s.toISOString()},x=await y(()=>{const u=p(),B=k.tag?u.items.filter(W=>W.tag!==k.tag):u.items,H=B.length<u.items.length;return u.items=[...B,k],l(h,u),{replaced:H}}),O=ct(s);return{queued:{...x,count:O.count,nextFlushAt:O.nextFlushAt,held:f,hold:d,sendFailed:w}}}i(vt,"deliver");async function At(t,e,n){return!t||!p().items.some(r=>r.tag===t)?null:y(()=>{const r=p(),s=r.items.find(o=>o.tag===t);return s?(s.body=e(s.body||""),n&&(s.url=n),l(h,r),s.body):null})}i(At,"reviseQueued");async function Pt(t,e){const n=new Set(t),r=i(s=>n.has(s.tag)&&typeof s.url=="string"&&s.url.startsWith(`/${e}/`),"matches");return p().items.some(r)?y(()=>{const{items:s}=p(),o=s.filter(d=>!r(d)),a=s.length-o.length;return a&&l(h,{items:o}),a}):0}i(Pt,"dropQueued");let I=null;function $t(){return I||=(async()=>{const t=await y(()=>{const o=p();return o.items.length&&l(h,{items:[]}),o.items});if(!t.length)return null;const e=[],n=[],r=[],s=new Map(t.map(o=>[o.id,o.kind]));for(const o of Z(t)){const a=await N(await L(s.get(o.id)),{title:o.title,body:o.body,url:o.url,tag:o.tag});r.push(...a),(Q(a)?e:n).push(o)}return n.length&&await y(()=>{const o=p(),a=new Set(o.items.map(f=>f.tag).filter(Boolean)),d=t.filter(f=>n.some(w=>w.id===f.id)&&!(f.tag&&a.has(f.tag)));l(h,{items:[...d,...o.items]})}),{count:t.length,sent:e.length,ok:!n.length,results:r}})().finally(()=>{I=null}),I}i($t,"flushQueue");export{M as PUSH_KINDS,ut as PushKindError,St as addSubscription,vt as deliver,Pt as dropQueued,$t as flushQueue,Ot as migrateSettings,ot as publicKey,ft as pushKindError,xt as pushRoute,ct as queueStatus,Et as readCodexShare,D as readSettings,j as readSettingsFile,At as reviseQueued,at as saveSettings,bt as sendAll,jt as setCodexShare,Bt as setNotifications,kt as subscriptions,U as writeSettingsFile};
package/hub/repos.mjs ADDED
@@ -0,0 +1,2 @@
1
+ var E=Object.defineProperty;var c=(t,e)=>E(t,"name",{value:e,configurable:!0});import h from"node:fs";import p from"node:path";import{spawn as N}from"node:child_process";import{reposDir as O}from"../shared/repos.mjs";import{withFileLock as D}from"./locks.mjs";import{storedPath as A}from"../bin/install.mjs";const m=c((t,e=400)=>{throw Object.assign(new Error(t),{status:e})},"refuse");function C(t){return(typeof t!="string"||t.length>140||!/^[a-z\d](?:[a-z\d-]{0,37}[a-z\d])?\/[a-z\d_.-]{1,100}$/i.test(t)||[".","..",".git"].includes(t.split("/")[1].toLowerCase()))&&m("Repo must be owner/name on GitHub."),t}c(C,"validateGithub");function S(t){return(typeof t!="string"||!t||t.length>255||t==="@"||t.startsWith("-")||/[\s\x00-\x1f\x7f~^:?*\[\\]/.test(t)||t.includes("..")||t.includes("@{")||t.split("/").some(e=>!e||e.startsWith(".")||e.endsWith(".")||e.endsWith(".lock")))&&m("Choose a valid branch name."),t}c(S,"validateBranch");const x=c((t,e)=>{try{return t(e),!0}catch{return!1}},"valid");function G(t){const e=/^(?:git@github\.com:|https:\/\/github\.com\/|ssh:\/\/git@github\.com\/)([^/\s]+\/[^/\s]+?)(?:\.git)?\/?$/i.exec(t.trim());return e?W(e[1]):null}c(G,"githubFromOrigin");function W(t){if(typeof t!="string")return null;const e=t.trim(),a=/^(?:https?:\/\/(?:www\.)?github\.com\/|git@github\.com:|ssh:\/\/git@github\.com\/)([^/\s?#]+\/[^/\s?#]+)(?:\/[^\s]*)?(?:[?#][^\s]*)?$/i.exec(e),n=a?a[1].replace(/\.git$/i,""):e;try{return C(n)}catch{return null}}c(W,"githubFromInput");async function Q(t,{repos:e,manifest:a,sendJSON:n}){try{return n(200,await e.lookup(t.searchParams.get("github"),a))}catch(i){return n(i.status||502,{error:i.message})}}c(Q,"repoLookupRoute");function T(t,e,a={}){return new Promise(n=>{const i=N(t,e,{...a,shell:!1,stdio:["ignore","pipe","pipe"]});let f="",y="";i.stdout.on("data",g=>{f+=g}),i.stderr.on("data",g=>{y+=g}),i.on("error",g=>n({status:1,stdout:f,stderr:g.message})),i.on("close",g=>n({status:g,stdout:f,stderr:y}))})}c(T,"runRepo");function v(t,e=O()){C(t);const a=p.resolve(e),n=p.resolve(a,t.toLowerCase());n.startsWith(`${a}${p.sep}`)||m("Clone path must stay inside .repos.");let i=a;for(const y of["",...t.toLowerCase().split("/")])y&&(i=p.join(i,y)),h.lstatSync(i,{throwIfNoEntry:!1})?.isSymbolicLink()&&m("Clone path must stay inside .repos; symlinks are refused.");let f=a;for(;!h.existsSync(f);)f=p.dirname(f);return h.realpathSync(f)!==f&&m("Clone path must stay inside .repos; symlinks are refused."),n}c(v,"repoPath");const L=c((t,e)=>(t.stderr||t.stdout||e).trim().split(`
2
+ `)[0].replace(/(\w+:\/\/)[^/\s@]+@/g,"$1"),"firstLine"),z=c(async(t,e,a,n)=>{const i=await t(e,a,n);return i.status!==0&&m(L(i,`${e} failed`),502),i.stdout},"checked"),$=c(async(t,e)=>JSON.parse(await z(t,"gh",e)),"ghJSON");async function B(t,e,a){h.lstatSync(p.join(e,".git"),{throwIfNoEntry:!1})?.isDirectory()||m("The clone path must have a real .git metadata directory; symlinks are refused.");const n=await t("git",["-C",e,"rev-parse","--show-toplevel"]);(n.status!==0||p.resolve(n.stdout.trim())!==h.realpathSync(e))&&m("The clone path must be the root of a Git work tree.");const i=await t("git",["-C",e,"remote","get-url","origin"]);(i.status!==0||G(i.stdout)?.toLowerCase()!==a.toLowerCase())&&m(`The clone path origin must be github.com/${a}.`)}c(B,"verifyClone");const j=c((t,e)=>t("git",["-c","credential.helper=",...e],{env:{...process.env,GIT_TERMINAL_PROMPT:"0"}}),"publicGit");async function P(t,e,a,n){const i=await t("git",["-C",e,"fetch","--no-tags","origin",`+refs/heads/${n}:refs/remotes/origin/${n}`]);i.status!==0&&m(`Could not fetch branch "${n}" from ${a}: ${L(i,"git fetch failed")}`,502)}c(P,"fetchBranch");function U({run:t=T,base:e=O(),now:a=Date.now}={}){let n,i;const f=new Map,y=c(s=>({github:s.nameWithOwner,private:s.isPrivate,pushedAt:s.pushedAt||"",defaultBranch:s.defaultBranchRef?.name||"",description:s.description||""}),"repoRow"),g=c((s,l)=>{const o=v(s.github,e),r=Object.entries(l.sites||{}).filter(([,u])=>u.github?.toLowerCase()===s.github.toLowerCase()&&typeof u.owner?.repo=="string"&&A(p.resolve(u.owner.repo)).toLowerCase()===A(o).toLowerCase()).map(([u,d])=>({route:u,title:d.title||u}));return{...s,clone:o,cloned:h.lstatSync(p.join(o,".git"),{throwIfNoEntry:!1})?.isDirectory()||!1,shared:r}},"decorate");async function R(s){const l=await j(t,["ls-remote","--symref",`https://github.com/${s}.git`,"HEAD","refs/heads/*"]);if(l.status!==0)return null;const o=l.stdout,r=[...new Set([...o.matchAll(/^[\da-f]+\s+refs\/heads\/(.+)$/gm)].map(d=>d[1]).filter(d=>x(S,d)))],u=/^ref:\s+refs\/heads\/(.+)\s+HEAD$/m.exec(o)?.[1];return{defaultBranch:r.includes(u)?u:r[0]||"",branches:r}}c(R,"remoteBranches");async function I(s){const l=await t("gh",["repo","view",s,"--json","nameWithOwner,isPrivate,description,pushedAt,defaultBranchRef"]);if(l.status===0){const r=JSON.parse(l.stdout);return C(r.nameWithOwner),y(r)}return(await j(t,["ls-remote","--heads",`https://github.com/${s}.git`])).status!==0?null:{github:s,private:!1,pushedAt:"",defaultBranch:"",description:""}}c(I,"find");async function _(){if((await t("gh",["auth","status"])).status!==0)return{signedIn:!1,repos:[]};const l=await $(t,["api","user"]),o=await $(t,["api","user/orgs","--paginate","--slurp"]),r=[...new Set([l.login,...o.flat().map(w=>w.login)])],u=await Promise.all(r.map(w=>$(t,["repo","list",w,"--limit","100000","--json","nameWithOwner,isPrivate,pushedAt,defaultBranchRef,description"]))),d=new Map;for(const w of u.flat())x(C,w.nameWithOwner)&&d.set(w.nameWithOwner.toLowerCase(),y(w));return{signedIn:!0,login:l.login,owners:r,repos:[...d.values()].sort((w,k)=>(k.pushedAt||"").localeCompare(w.pushedAt||"")||w.github.localeCompare(k.github))}}return c(_,"discover"),{path:c(s=>v(s,e),"path"),cloned:c(s=>h.lstatSync(p.join(v(s,e),".git"),{throwIfNoEntry:!1})?.isDirectory()||!1,"cloned"),async list(s={},{fresh:l=!1}={}){return(l||!n||a()-n.at>=6e4)&&(i||=_().then(o=>(n={at:a(),value:o},o)).finally(()=>{i=null}),await i),{...n.value,repos:n.value.repos.map(o=>g(o,s))}},async lookup(s,l={}){const o=W(s);o||m("Repo must be owner/name on GitHub.");const r=o.toLowerCase();let u=f.get(r);if(!u||a()-u.at>=6e4){for(const[w,k]of f)a()-k.at>=6e4&&f.delete(w);u={at:a(),pending:I(o)},f.set(r,u),u.pending.catch(()=>{f.get(r)===u&&f.delete(r)})}const d=await u.pending;return d||m("Not found, or you don't have access.",404),g({...d,listed:!1},l)},async branches(s){C(s);try{const l=await $(t,["api",`repos/${s}`]),o=await $(t,["api",`repos/${s}/branches?per_page=100`,"--paginate","--slurp"]);return{defaultBranch:l.default_branch,branches:o.flat().map(r=>r.name).filter(r=>x(S,r))}}catch(l){const o=await R(s);if(!o)throw l;return o}},async ensure(s,l="main"){const o=v(s,e);return S(l),h.mkdirSync(p.dirname(o),{recursive:!0}),D(p.join(p.resolve(e),".locks",s.toLowerCase()),async()=>{if(v(s,e),h.existsSync(o))return await B(t,o,s),await P(t,o,s,l),o;const r=h.mkdtempSync(p.join(p.resolve(e),".clone-"));try{const u=await t("gh",["repo","clone",s,p.join(r,"repo")]);u.status!==0&&(h.rmSync(p.join(r,"repo"),{recursive:!0,force:!0}),(await j(t,["clone",`https://github.com/${s}.git`,p.join(r,"repo")])).status!==0&&m(L(u,"gh failed"),502)),await B(t,p.join(r,"repo"),s),await P(t,p.join(r,"repo"),s,l),v(s,e),h.renameSync(p.join(r,"repo"),o)}finally{h.rmSync(r,{recursive:!0,force:!0})}return o},{timeout:10*6e4,heartbeat:!0})}}}c(U,"repoService");export{W as githubFromInput,G as githubFromOrigin,Q as repoLookupRoute,v as repoPath,U as repoService,T as runRepo,S as validateBranch,C as validateGithub};
package/hub/routes.mjs ADDED
@@ -0,0 +1 @@
1
+ var r=Object.defineProperty;var o=(e,t)=>r(e,"name",{value:t,configurable:!0});const a=/^[a-z0-9][a-z0-9-]{0,40}$/,c=o(e=>typeof e=="string"&&a.test(e),"validRoute");function p(e){return e.toLowerCase().replace(/[^a-z0-9]+/g,"-").replace(/^-+|-+$/g,"").slice(0,41).replace(/-+$/,"")}o(p,"routeFromName");const s=o((e,t)=>Object.hasOwn(e||{},t)?e[t]:void 0,"routeRecord");export{a as ROUTE_RE,p as routeFromName,s as routeRecord,c as validRoute};
package/hub/rules.mjs ADDED
@@ -0,0 +1 @@
1
+ var I=Object.defineProperty;var o=(t,r)=>I(t,"name",{value:r,configurable:!0});import h from"node:fs";import y from"node:path";import j from"node:os";import{readSettingsFile as S,writeSettingsFile as E}from"./push.mjs";const w=["owner","claude"],$=400,b=50;class D extends Error{static{o(this,"RuleError")}status=400}const s=o(t=>{throw new D(t)},"fail");function p(t,r="a rule"){const e=typeof t=="string"?t.replace(/\s+/g," ").trim():"";return e||s(`${r} must be non-empty text`),e.length>$&&s(`${r} has ${e.length} characters; a rule takes at most ${$}`),e}o(p,"ruleText");const J=o(t=>typeof t?.id=="string"&&typeof t.text=="string"&&t.text.trim()!=="","isRule"),L=o(t=>Array.isArray(t?.rules)?t.rules.filter(J):[],"rulesOf"),R=o(()=>"r"+Date.now().toString(36)+Math.random().toString(36).slice(2,5),"newId"),x=o(t=>typeof t=="string"&&/^[\w-]{1,40}$/.test(t)?t:s(`a rule id is letters, digits, "_" and "-", not "${t}"`),"ruleId"),O=o((t,r=1)=>t.length+r<=b?t:s(`a list takes at most ${b} rules`),"room");function v(t,{text:r,by:e="owner"},i=new Date){w.includes(e)||s(`--by takes ${w.join(" or ")}`);const u={id:R(),text:p(r,"--text"),by:e,at:i.toISOString()};return{rules:[...O(t),u],rule:u}}o(v,"addRule");function B(t,r,e,i=new Date){const u=t.find(a=>a.id===x(r))||s(`no rule "${r}"`),l={...u,text:p(e,"--text"),at:i.toISOString()};return{rules:t.map(a=>a===u?l:a),rule:l}}o(B,"editRule");function H(t,r){const e=t.find(i=>i.id===x(r))||s(`no rule "${r}"`);return{rules:t.filter(i=>i!==e),rule:e}}o(H,"removeRule");function X(t,r,e=[],i=new Date){Array.isArray(r)||s("rules must be a JSON array"),(!Array.isArray(e)||e.some(n=>typeof n!="string"))&&s("base must be a JSON array of rule ids");const u=new Map(t.map(n=>[n.id,n])),l={added:[],edited:[],removed:[]},a=new Set,m=r.map((n,f)=>{(!n||typeof n!="object"||Object.keys(n).some(g=>g!=="id"&&g!=="text"))&&s(`rules[${f}] takes only id and text`);const c=p(n.text,`rules[${f}].text`),d=n.id===void 0?null:u.get(x(n.id));return!d||a.has(d.id)?(l.added.push(c),{id:R()+f.toString(36),text:c,by:"owner",at:i.toISOString()}):(a.add(d.id),d.text===c?d:(l.edited.push({from:d.text,to:c}),{...d,text:c,at:i.toISOString()}))}),A=new Set(e);for(const n of t)a.has(n.id)||(A.has(n.id)?l.removed.push(n.text):m.push(n));return{rules:O(m,0),changes:l}}o(X,"applyRules");const T=o(t=>!!(t.added.length||t.edited.length||t.removed.length),"changed"),Y=o(()=>L(S()),"readHubRules");function q(t){const r=y.join(j.homedir(),".claude","kickflip","settings.json");let e=null;try{e=h.readFileSync(r,"utf8")}catch{}if(e!==null)try{JSON.parse(e)}catch{s(`${r} is not valid JSON; fix it before changing the hub rules`)}h.mkdirSync(y.dirname(r),{recursive:!0}),E({...S(),rules:t})}o(q,"writeHubRules");const k=o(t=>t.replace(/\s+/g," ").trim(),"flat"),z=o((t,r)=>[...t.map(e=>`hub ${k(e.text)}`),...r.map(e=>`board ${k(e.text)}`)],"ruleLines");export{b as RULES_MAX,w as RULE_BY,$ as RULE_MAX,D as RuleError,v as addRule,X as applyRules,T as changed,B as editRule,Y as readHubRules,H as removeRule,z as ruleLines,p as ruleText,L as rulesOf,q as writeHubRules};
@@ -0,0 +1 @@
1
+ var O=Object.defineProperty;var f=(e,n)=>O(e,"name",{value:n,configurable:!0});const c=Object.freeze({windows:Object.freeze([Object.freeze({from:"08:00",every_minutes:15}),Object.freeze({from:"18:00",every_minutes:30}),Object.freeze({from:"22:00",every_minutes:60})]),hold_overnight:!0,wakes:Object.freeze(["08:00","09:00"]),notifications:Object.freeze({on:!0,weekends_off:!1,snoozed_until:null})}),h="never",w=8,v=8,g=/^([01]\d|2[0-3]):([0-5]\d)$/,k=2*6e4,d=f(e=>{const[n,t]=e.split(":").map(Number);return n*60+t},"minutes"),z=f(e=>e.getHours()*60+e.getMinutes()+e.getSeconds()/60+e.getMilliseconds()/6e4,"minuteOfDay"),l=f((e,n,t=0)=>{const[o,r]=n.split(":").map(Number);return new Date(e.getFullYear(),e.getMonth(),e.getDate()+t,o,r)},"atOn"),_=f((e,n)=>{const t=l(e,n);return t>e?t:l(e,n,1)},"nextAt"),N=f((e,n)=>{const t=l(e,n);return t<=e?t:l(e,n,-1)},"lastAt"),b=f(e=>({windows:e.windows.map(n=>({from:n.from,every_minutes:n.every_minutes})),hold_overnight:e.hold_overnight,wakes:[...e.wakes],notifications:{...e.notifications}}),"copy"),E=30*864e5;function x(e){const n=e?.on??c.notifications.on,t=e?.weekends_off??c.notifications.weekends_off,o=e?.snoozed_until??null;return typeof n!="boolean"?{error:"Notifications must be on or off."}:typeof t!="boolean"?{error:"Off on weekends must be on or off."}:o!==null&&(typeof o!="string"||Number.isNaN(Date.parse(o)))?{error:"Snooze must end at a date and time."}:{notifications:{on:n,weekends_off:t,snoozed_until:o===null?null:new Date(o).toISOString()}}}f(x,"validateNotifications");function S(e){const n=e?.windows,t=e?.hold_overnight??c.hold_overnight,o=e?.wakes??c.wakes,{notifications:r,error:s}=x(e?.notifications);if(s)return{error:s};if(typeof t!="boolean")return{error:"Hold pushes overnight must be on or off."};if(!Array.isArray(o))return{error:"Wake times must be a list of HH:MM times."};if(o.length>v)return{error:`Use at most ${v} wake times.`};for(const[a,u]of o.entries()){if(typeof u!="string"||!g.test(u))return{error:`Wake ${a+1} must be HH:MM (24-hour).`};if(o.indexOf(u)!==a)return{error:`Two wakes are at ${u}; keep one.`}}if(!Array.isArray(n)||!n.length)return{error:"Add at least one window."};if(n.length>w)return{error:`Use at most ${w} windows.`};const i=[];for(const[a,u]of n.entries()){if(typeof u?.from!="string"||!g.test(u.from))return{error:`Window ${a+1}'s start must be HH:MM (24-hour).`};const m=Number(u.every_minutes);if(u.every_minutes===""||u.every_minutes==null||!Number.isInteger(m)||m<5||m>120)return{error:`Window ${a+1}'s interval must be a whole number of minutes from 5 to 120.`};if(i.some(D=>D.from===u.from))return{error:`Two windows start at ${u.from}; give each its own start.`};i.push({from:u.from,every_minutes:m})}return{settings:{windows:i.sort((a,u)=>d(a.from)-d(u.from)),hold_overnight:t,wakes:[...o].sort((a,u)=>d(a)-d(u)),notifications:r}}}f(S,"validate");function T(e){const n=e?.immediate?.start,t=e?.immediate?.end,o=e?.bundle?.until;if(![n,t,o].every(a=>typeof a=="string"&&g.test(a)))return null;const r=Number(e.bundle.every_minutes),s=[{from:n,every_minutes:c.windows[0].every_minutes}];t!==n&&o!==t&&s.push({from:t,every_minutes:Number.isInteger(r)?r:c.windows[1].every_minutes});const i=o===t?t:o;return i!==n&&!s.some(a=>a.from===i)&&s.push({from:i,every_minutes:c.windows[2].every_minutes}),S({windows:s,hold_overnight:o!==n}).settings||null}f(T,"fromPush");function X(e){const n=x(e?.notifications).notifications||b(c).notifications,t=f(o=>o&&{...o,notifications:n},"withThem");return S({...e?.polling,notifications:n}).settings||t(T(e?.push))||t(b(c))}f(X,"pollingOf");function p(e,n){const t=n.windows,o=z(e);let r=t.length-1;for(let a=0;a<t.length;a++)d(t[a].from)<=o&&(r=a);const s=t[r],i=t[(r+1)%t.length];return{from:s.from,every_minutes:s.every_minutes,startedAt:N(e,s.from),endsAt:_(e,i.from)}}f(p,"windowAt");function y(e,n){const t=e.windows;if(t.length===1)return 1440;const o=d(t[(n+1)%t.length].from)-d(t[n].from);return o>0?o:o+1440}f(y,"windowSpan");const H=f((e,n)=>Math.ceil(y(e,n)/e.windows[n].every_minutes),"checksIn"),Z=f(e=>e.windows.reduce((n,t,o)=>n+H(e,o),0),"checksADay"),A=f(e=>e.hold_overnight&&e.windows.length>1,"holdsOvernight");function K(e){const n=e.windows.length-1;return e.windows.flatMap((t,o)=>{const r=d(t.from),s=r+y(e,o),i=A(e)&&o===n;return e.windows.length===1?[{window:o,start:0,end:1440,held:i}]:s<=1440?[{window:o,start:r,end:s,held:i}]:[{window:o,start:r,end:1440,held:i},{window:o,start:0,end:s-1440,held:i}]})}f(K,"daySegments");function j(e,n){if(e.windows.length>=w)return null;const t=d(e.windows[n].from),o=y(e,n),r=Math.round((t+o/2)/15)*15;if(r<=t||r>=t+o)return null;const s=r%1440;return`${String(Math.floor(s/60)).padStart(2,"0")}:${String(s%60).padStart(2,"0")}`}f(j,"splitPoint");function Y(e,{op:n,w:t,value:o}){const r=e.windows.map(u=>({from:u.from,every_minutes:u.every_minutes})),s={windows:r,hold_overnight:e.hold_overnight,wakes:[...e.wakes]},i=r.findIndex(u=>u.from===t);if(["from","every","split","remove"].includes(n)&&i<0)return{error:"That window has changed. Pick it again on the bar."};const a=i<0?e.windows[0].from:t;switch(n){case"from":return r[i].from=o,{input:s,select:o};case"every":return r[i].every_minutes=o,{input:s,select:a};case"split":{const u=j(e,i);return u?(r.push({from:u,every_minutes:r[i].every_minutes}),{input:s,select:u}):{error:r.length>=w?`Use at most ${w} windows.`:"This window is too short to split."}}case"remove":{if(r.length<2)return{error:"Keep at least one window."};const u=r[(i-1+r.length)%r.length].from;return r.splice(i,1),{input:s,select:u}}case"hold":return s.hold_overnight=o==="on",{input:s,select:a};case"wake-add":return s.wakes.push(o),{input:s,select:a};case"wake-remove":return s.wakes=s.wakes.filter(u=>u!==o),{input:s,select:a};default:return{error:"That change is not one the Polling modal makes."}}}f(Y,"editPolling");function I(e,n){if(!A(n))return!1;const t=p(e,n);return t.from===n.windows.at(-1).from&&e-t.startedAt>k}f(I,"holdsPushes");function M(e,n){if(!n.notifications?.weekends_off)return null;const[t,o]=[n.windows[0].from,n.windows.at(-1).from];let r=l(e,o,-((e.getDay()-5+7)%7));r>e&&(r=l(r,o,-7));const s=l(r,t,3);return e-r>k&&e<s?s:null}f(M,"weekendEnd");function W(e,n){if(n.notifications?.on===!1)return{until:h,why:"off"};const t=n.notifications?.snoozed_until?new Date(n.notifications.snoozed_until):null,o=f(i=>(t>i?["snoozed",t]:null)||M(i,n)&&["weekend",M(i,n)]||I(i,n)&&["overnight",p(i,n).endsAt],"at"),r=o(e);if(!r)return null;let s=r[1];for(let i=o(s),a=0;i&&a<8;i=o(s),a++)s=i[1];return{until:s,why:r[0]}}f(W,"hold");function P(e,n){return W(e,n)?.until??null}f(P,"heldUntil");function C(e,n){let t=$(e,n);for(let o=0;o<8;o++){const r=P(t,n);if(r===h)return h;if(!r)return t;t=$(new Date(r.getTime()-1),n)}return t}f(C,"flushAt");const U=/^\d{4}-\d{2}-\d{2}T\d{2}:\d{2}(:\d{2}(\.\d{1,3})?)?(Z|[+-]\d{2}:\d{2})?$/;function G(e,n,t){const o=String(n),[r,s]=[t.windows[0].from,t.windows.length];let i;if(o==="morning")i=_(e,r);else if(o==="tonight")i=s>1?_(e,t.windows[Math.max(1,s-2)].from):null;else if(o==="monday")i=l(e,r,(8-e.getDay())%7),i<=e&&(i=l(i,r,7));else if(U.test(o))i=new Date(o);else{const a=/^([1-9]\d{0,3})([mhdw])$/.exec(o);if(!a)return null;const u=Number(a[1]);i=new Date(e.getTime()+u*{m:6e4,h:36e5,d:864e5,w:7*864e5}[a[2]])}return i&&i>e&&i-e<=E?i:null}f(G,"snoozeEnd");function V(e,n=new Date){if(e.why==="off")return"notifications are off";const t=R(e.until,n);return e.why==="snoozed"?`snoozed until ${t}`:e.why==="weekend"?`held for the weekend until ${t}`:`held overnight until ${t}`}f(V,"holdLine");function $(e,n){const t=p(e,n),o=t.every_minutes*6e4,r=new Date(t.startedAt.getTime()+(Math.floor((e-t.startedAt)/o)+1)*o);return r<t.endsAt?r:t.endsAt}f($,"nextSlot");function F(e){return`${e.getFullYear()}-${String(e.getMonth()+1).padStart(2,"0")}-${String(e.getDate()).padStart(2,"0")}`}f(F,"localDay");function q(e,n,t={}){const o=F(e);return n.wakes.filter(r=>l(e,r)<=e&&t?.[r]!==o)}f(q,"wakesDue");function B(e){const n=new Map;return e.forEach((t,o)=>{t.tag&&n.set(t.tag,o)}),e.filter((t,o)=>!t.tag||n.get(t.tag)===o).map(t=>({id:t.id,title:t.title,body:t.body||"",url:t.url||"/",tag:t.tag||void 0}))}f(B,"notifications");function R(e,n=new Date){const t=e.getHours(),o=`${t%12||12}:${String(e.getMinutes()).padStart(2,"0")}${t<12?"am":"pm"}`,r=Math.round((l(e,"00:00")-l(n,"00:00"))/864e5);return r===0?o:r===1?`${o} tomorrow`:`${o} ${e.toDateString().slice(0,10)}`}f(R,"clock");export{c as DEFAULTS,h as NEVER,Z as checksADay,H as checksIn,R as clock,K as daySegments,Y as editPolling,C as flushAt,T as fromPush,P as heldUntil,W as hold,V as holdLine,A as holdsOvernight,I as holdsPushes,F as localDay,$ as nextSlot,B as notifications,X as pollingOf,G as snoozeEnd,j as splitPoint,S as validate,q as wakesDue,p as windowAt,y as windowSpan};
@@ -0,0 +1,2 @@
1
+ var K=Object.defineProperty;var c=(e,s)=>K(e,"name",{value:s,configurable:!0});import O from"node:fs";import h from"node:path";import j from"node:os";import{spawn as D}from"node:child_process";import{fileURLToPath as F}from"node:url";import{routeRecord as m,validRoute as J}from"./routes.mjs";import{withFileLock as W}from"./locks.mjs";import{RESUME_STATE as C,carryCursor as H,hubBoard as Q,isListening as q,listBoards as P,oldestUnheard as Y,readManifest as $,readJSON as z,saveJSON as V,watchedCounts as X,watchedLines as Z,watcherCursor as ee,withManifestLock as se,writeManifest as te}from"./boards.mjs";import{readUsageGate as oe,heldUsageEvent as ne}from"../shared/usage-gate.mjs";import{liveSessions as y,sessionRecords as re,processStarts as ie,verifiedProcess as ae}from"./live.mjs";import{deliver as ce}from"./push.mjs";import{WAIT_MS as de}from"./prompts.mjs";import{storedPath as U}from"../bin/install.mjs";const v=U(h.resolve(h.dirname(F(import.meta.url)),"..")),ue=3*6e4,le=5*6e4,fe=15*6e4;function Me(e,s=Date.now()){return Object.entries(e.starting||{}).filter(([t])=>!m(e.sites,t)).map(([t,o])=>{const i=o.asked_at||o.since;return{route:t,title:o.title||o.label||t,repo:o.repo,...o.phase?{phase:o.phase}:{},askedAt:i,failed:!!o.error||s-Date.parse(i)>=fe,...o.error?{error:o.error}:{}}})}c(Me,"startingBoards");const pe=de/1e3+60,me=JSON.stringify({worktree:{bgIsolation:"none"},hooks:{PermissionRequest:[{matcher:"*",hooks:[{type:"command",command:`"${process.execPath}" "${h.join(v,"hub","permission-hook.mjs")}"`,timeout:pe}]}]}});class l extends Error{static{c(this,"SessionError")}constructor(s,t){super(s),this.reason=t}}function he(e,s){if(!J(e))throw new l("Choose a board route.","none");return W(h.join(j.homedir(),".claude","kickflip","session-locks",e),()=>s({resume:c(t=>Te(e,t),"resume")}),{staleAfter:12e4,timeout:9e4})}c(he,"withSessionLock");function S(){if(process.env.KICKFLIP_CLAUDE)return process.env.KICKFLIP_CLAUDE;const e=h.join(j.homedir(),".local","bin","claude");return O.existsSync(e)?e:"claude"}c(S,"claudeBin");const R=c(e=>e==="CLAUDECODE"||e==="CLAUDE_PID"||e==="CLAUDE_EFFORT"||e.startsWith("CLAUDE_CODE_"),"inherited");function E(e,{cwd:s,timeout:t=6e4}){const o=S(),i=Object.fromEntries(Object.entries(process.env).filter(([n])=>!R(n)));return i.CLAUDE_BG_ISOLATION="none",h.isAbsolute(o)&&(i.PATH=`${h.dirname(o)}:${i.PATH||""}`),new Promise(n=>{let u="",d="",r;try{r=D(o,e,{cwd:s,env:i,detached:!0,stdio:["ignore","pipe","pipe"]})}catch(a){n({status:1,stdout:u,stderr:a.message});return}const f=c(a=>{clearTimeout(p),r.stdout.destroy(),r.stderr.destroy(),n({status:a,stdout:u,stderr:d})},"done"),p=setTimeout(()=>{try{r.kill("SIGKILL")}catch{}f(1)},t);r.stdout.on("data",a=>{u+=a}),r.stderr.on("data",a=>{d+=a}),r.on("error",a=>{d+=a.message,f(1)}),r.on("close",a=>f(a??1)),r.on("exit",a=>setTimeout(()=>f(a??1),300)),r.unref()})}c(E,"runClaude");async function we(e,{cwd:s,run:t=E,starts:o=ie,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 l(`claude stop failed: ${(n.stderr||n.stdout).trim().slice(0,300)}`,"failed")}else{if(!ae(e,o))throw new l(`Cannot verify session ${e.sessionId}'s process identity; refusing to signal pid ${e.pid}.`,"failed");i(e.pid,"SIGTERM")}}c(we,"runSessionStop");async function Ke(e,{live:s=y,stop:t=we}={}){if(!e?.session)return;const o=s().get(e.session);if(!o)return;await t(o,{cwd:e.repo||o.cwd||j.homedir()});const i=Date.now()+5e3;for(;s().get(e.session)?.pid===o.pid;){if(Date.now()>=i)throw new l(`session ${e.session} did not stop`,"failed");await new Promise(n=>setTimeout(n,100))}}c(Ke,"stopSession");function ge(e,{cwd:s}){const t=Object.fromEntries(Object.entries(process.env).filter(([n])=>!R(n)&&n!=="CLAUDE_BG_ISOLATION")),o=c(()=>{},"ignore");process.on("SIGINT",o).on("SIGQUIT",o);const i=c(()=>process.off("SIGINT",o).off("SIGQUIT",o),"settle");return new Promise((n,u)=>{const d=D(S(),e,{cwd:s,env:t,stdio:"inherit"});d.on("error",r=>{i(),u(new l(r.message,"failed"))}),d.on("close",r=>{i(),n({status:r??1})})})}c(ge,"runForegroundClaude");const N=c(({name:e,model:s})=>["--name",e,"--permission-mode","auto","--settings",me,...s?["--model",s]:[]],"common"),be=c(({route:e,name:s,model:t,prompt:o})=>["--bg",...N({name:s,model:t}),`/kickflip ${e}${o?` ${o}`:""}`],"startArgv"),$e=c((e,s)=>`The hub resumed this session because /${e} had nobody listening. Rejoin the board as session ${s} (kickflip steps 3 and 5): rerun \`node ${h.join(v,"bin","kickflip.mjs")} join ${e} --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"),ke=c(({id:e,route:s,name:t,model:o,background:i})=>["--resume",e,"--bg",...i?[]:N({name:t,model:o}),$e(s,e)],"resumeArgv"),B=c(e=>`You are the hub session. Read \`${h.join(v,"docs","hub-session.md")}\`, then rejoin /${e} and relaunch its watcher.`,"hubPrompt"),ye=c(({route:e,id:s,model:t})=>[...s?["--resume",s]:[],...t?["--model",t]:[],s?`${B(e)} If your session id (the folder above your scratchpad) isn't ${s}, rejoin with yours and --force.`:`/kickflip ${e} --hub
2
+ ${B(e)}`],"hubArgv");async function Fe({manifest:e=$(),repo:s,run:t=ge,live:o=y}={}){const i=Q(e);if(!i)throw new l("No hub board is flagged; flag one with kickflip set <route> --hub, or join one with kickflip join hub-orchestrator --template board --hub --session <id>","hub");const n=i.owner?.session?i.owner:null;if(n&&!n.repo&&!s)throw new l(`/${i.route}'s session ${n.session} has no folder recorded; pass --repo <its folder>`,"none");x(n,o);const u=n?.repo||h.resolve(s||process.cwd());if(!O.statSync(u,{throwIfNoEntry:!1})?.isDirectory())throw new l(n?.repo?`/${i.route}'s recorded folder ${u} is gone; rejoin from its new folder with --repo`:`no folder at ${u}`,"none");return t(ye({route:i.route,id:n?.session,model:n?.model}),{cwd:u})}c(Fe,"hubSession");function A(e,s){const t=m(e?.sites,s),o=t?.owner?.session?t.owner:m(e?.starting,s);return o?.session?{...o,title:t?.title||s}:null}c(A,"sessionOf");async function G(e,s=null){const t=e.match(/backgrounded\s*·\s*([0-9a-f]{8})\b/)?.[1]||e.match(/\bclaude attach ([0-9a-f]{8})\b/)?.[1];if(!t)return{session:s,confirmed:!1};if(s?.startsWith(t))return{session:s,confirmed:!0};for(let o=0;o<20;o++){const i=re().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}}c(G,"startedId");async function Ie(e,{run:s,cwd:t}){const o=await s(["agents","--json","--all"],{cwd:t});try{return JSON.parse(o.stdout).some(i=>i?.sessionId===e&&i.kind==="background")}catch{return!1}}c(Ie,"isBackground");function x(e,s){const t=e&&s().get(e.session);if(t)throw new l(`session ${e.session} is live (${t.kind==="bg"?"in the background":"in a terminal"}, pid ${t.pid}); it gets no second process`,"live")}c(x,"refuseLive");async function Se(e,{from:s,to:t,mark:o}){await se(async()=>{const i=$(),n=P(i).find(d=>d.route===e);if(n?.owner?.session===s&&(o||t!==s)&&H(n,{from:s,to:t,mark:o}),t===s)return;const u=m(i.sites,e);if(u?.owner?.session===s)Object.assign(u,{owner:{...u.owner,session:t},updated:new Date().toISOString()});else if(m(i.starting,e)?.session===s)i.starting[e]={...m(i.starting,e),session:t};else return;await te(i)})}c(Se,"moveOwner");async function Je(e,{manifest:s=$(),repo:t,model:o,prompt:i,run:n=E,live:u=y}={}){if(m(s.closed,e))throw new l(`/${e} is closed; use kickflip reopen ${e}`,"none");const d=A(s,e);x(d,u);const r=h.resolve(t||d?.repo||process.cwd());if(!O.statSync(r,{throwIfNoEntry:!1})?.isDirectory())throw new l(`no folder at ${r}`,"none");const f=m(s?.sites,e)?.title||m(s?.starting,e)?.title||e,p=be({route:e,name:f,model:o||d?.model,prompt:i}),a=await n(p,{cwd:r});if(a.status!==0)throw new l(`claude --bg failed (${a.status}): ${(a.stderr||a.stdout).trim().slice(0,300)}`,"failed");const{session:g,confirmed:k}=await G(a.stdout);if(!g)throw new l(`claude --bg started a session but no session record names it yet; find it with claude agents (${a.stdout.trim().slice(0,300)})`,"failed");return{route:e,session:g,confirmed:k,repo:U(r),model:o||d?.model||null,name:f,argv:[S(),...p],output:a.stdout.trim()}}c(Je,"startSession");function _e(e,s={}){return he(e,({resume:t})=>t(s))}c(_e,"resumeSession");async function Te(e,{manifest:s=$(),run:t=E,live:o=y,mark:i=null}={}){if(m($().closed,e))throw new l(`/${e} is closed; use kickflip reopen ${e}`,"none");const n=A(s,e);if(!n)throw new l(`/${e} has no recorded session`,"none");if(!n.repo)throw new l(`/${e}'s session ${n.session} has no folder recorded; its session records one by rejoining (kickflip join, --repo)`,"none");x(n,o);const u=await Ie(n.session,{run:t,cwd:n.repo}),d=ke({id:n.session,route:e,name:n.title,model:n.model,background:u}),r=await t(d,{cwd:n.repo});if(r.status!==0)throw new l(`claude --resume failed (${r.status}): ${(r.stderr||r.stdout).trim().slice(0,300)}`,"failed");const{session:f,confirmed:p}=await G(r.stdout,n.session);return await Se(e,{from:n.session,to:f,mark:i}),{route:e,session:f,previous:n.session,confirmed:p,repo:n.repo,argv:[S(),...d],output:r.stdout.trim()}}c(Te,"resumeRecordedSession");function Oe(e,{listening:s,live:t,recorded:o,counts:i,waiting:n,now:u}){if(s)return{action:"none",entry:null};const d=new Date(u).toISOString(),r=e?{...e}:{down_since:d};return t?delete r.mark:!r.mark&&i&&(r.mark=i),r.pushed_at?{action:"none",entry:r}:r.resumed_at?r.error||u-Date.parse(r.resumed_at)>=le?{action:"push",entry:{...r,pushed_at:d}}:{action:"none",entry:r}:!o||t?{action:"none",entry:r}:!!(e?.mark&&i&&Object.entries(i).some(([p,a])=>a>(m(e.mark,p)??0)))||n&&u-Date.parse(r.down_since)>=ue?{action:"resume",entry:{...r,resumed_at:d}}:{action:"none",entry:r}}c(Oe,"resumeStep");async function We({manifest:e=$(),now:s=new Date,live:t=y,listening:o=c((d,r)=>q(d,{now:s.getTime(),running:r}),"listening"),resume:i=c((d,r)=>_e(d,{manifest:e,live:t,mark:r}),"resume"),push:n=ce,gate:u=oe({now:s})}={}){const d=z(C,{}),r=t(),f=Object.create(null),p={resumed:[],pushed:[],failed:[]};for(const a of P(e)){const g=A(e,a.route),k=!!(g&&r.has(g.session)),_=o(a,r);let{action:I,entry:w}=Oe(m(d,a.route),{listening:_,live:k,recorded:!!g?.repo,now:s.getTime(),counts:_?null:X(a),waiting:!_&&!k&&Y(a)!==null});if(I==="resume"&&u.state==="paused"){const b=m(d,a.route)?.mark||ee(a);Object.entries(Z(a)).flatMap(([T,M])=>M.slice(m(b,T)||0)).some(T=>!ne(T))||(I="none",delete w.resumed_at)}if(I==="resume")try{await i(a.route,w.mark),p.resumed.push(a.route)}catch(b){w={...w,error:b.message},p.failed.push(`${a.route}: could not resume its session (${b.message})`)}else if(I==="push"){const b=w.error?`The hub couldn't resume it: ${w.error}`:k?"Its session is running but hasn't relaunched its watcher.":`It still isn't listening after the hub resumed it. Try kickflip resume ${a.route}, or claude agents.`;try{await n({title:`${a.title}'s session didn't come back`,body:b,url:`/${a.route}/`,tag:`resume-${a.route}`},{kind:"fault",immediate:!0,at:s}),p.pushed.push(a.route)}catch(L){w={...w,pushed_at:void 0},p.failed.push(`${a.route}: could not push that its session didn't come back (${L.message})`)}}w&&(f[a.route]=w)}return JSON.stringify(f)!==JSON.stringify(d)&&V(C,f),p}c(We,"checkSessions");export{me as BG_SETTINGS,le as COMEBACK_WAIT,pe as PROMPT_HOOK_TIMEOUT_S,ue as RESUME_AFTER,fe as STARTING_WAIT,l as SessionError,We as checkSessions,S as claudeBin,ye as hubArgv,Fe as hubSession,ke as resumeArgv,$e as resumePrompt,_e as resumeSession,Oe as resumeStep,E as runClaude,ge as runForegroundClaude,we as runSessionStop,A as sessionOf,be as startArgv,Je as startSession,Me as startingBoards,Ke as stopSession,he as withSessionLock};
package/hub/shell.mjs ADDED
@@ -0,0 +1 @@
1
+ var h=Object.defineProperty;var s=(t,o)=>h(t,"name",{value:o,configurable:!0});import u from"node:fs";import i from"node:path";import{fileURLToPath as b}from"node:url";const f=i.dirname(b(import.meta.url)),p=i.join(f,"..","board","public"),m=[p,i.join(f,"..","starter","public")],_=i.join(f,"..","shared","colors.css"),A=new Set(["","settings.html","questions.html","notes.html"]),d=s(t=>t===""||t.endsWith("/")||/\.html?$/.test(t),"isPagePath"),S=new Set(["/","/_hub/","/_hub/settings","/_hub/settings/polling"]),E=s(t=>t["x-app-shell"]==="1"?"document":t["sec-fetch-dest"],"destOf");function O({method:t="GET",pathname:o="/",search:e="",dest:l,sites:c=new Set}){if(S.has(o))return t==="GET"||t==="HEAD"?"shell":"hub";if(o==="/_hub"||o.startsWith("/_hub/")||o==="/sw.js"||o==="/manifest.webmanifest"||o==="/favicon.ico")return"hub";if(t!=="GET"||l!=="document"||new URLSearchParams(e).has("alone"))return"forward";const r=/^\/([^/]+)\/(.*)$/.exec(o);let n=null;try{n=r?decodeURIComponent(r[1]):null}catch{n=null}return r&&/^try(?:\/|$)/.test(r[2])?"forward":n!==null&&c.has(n)&&d(r[2])?"shell":"forward"}s(O,"pageOf");function g(t,o){return t.replace(/\b(src|href)="\/_hub\/app\/([\w.-]+\.(?:js|css))"/g,(e,l,c)=>{const r=o(c);return r?`${l}="/_hub/app/${c}?v=${r}"`:e})}s(g,"versionAppAssets");function w(t){if(!/^[\w.-]+\.(?:js|css|png)$/.test(t))return null;for(const o of m){const e=i.join(o,t);try{if(u.statSync(e).isFile())return e}catch{}}return null}s(w,"appFile");const a=s(t=>{try{const o=u.statSync(t);return`${Math.floor(o.mtimeMs).toString(36)}-${o.size.toString(36)}`}catch{return null}},"fileVersion"),x=s(()=>a(_)||"0","colorsVersion");function R({sidebar:t="",sidebarData:o="null",touchIcon:e="/_hub/icon-180.png"}={}){const l=g(u.readFileSync(i.join(p,"app.html"),"utf8"),r=>{const n=w(r);return n&&a(n)}),c={sidebar:t,sidebarData:o,touchIcon:e,colorsVersion:x()};return l.replace(/\{\{(sidebar|sidebarData|touchIcon|colorsVersion)\}\}/g,(r,n)=>c[n])}s(R,"shellHtml");export{p as BOARD_PUBLIC,A as VIEW_PAGES,w as appFile,x as colorsVersion,E as destOf,a as fileVersion,O as pageOf,R as shellHtml,g as versionAppAssets};