@lovek4d/kickflip 0.7.1 → 0.7.4

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 (62) hide show
  1. package/CONTEXT.md +25 -21
  2. package/SKILL.md +13 -9
  3. package/bin/event-lines.mjs +1 -1
  4. package/bin/install.mjs +3 -3
  5. package/bin/kickflip.mjs +1 -1
  6. package/bin/wait-events.mjs +2 -2
  7. package/board/bin/board.mjs +68 -50
  8. package/board/domain.mjs +7 -7
  9. package/board/finish.mjs +4 -4
  10. package/board/headless.mjs +8 -8
  11. package/board/hub-lines.mjs +1 -1
  12. package/board/onboarding.mjs +3 -3
  13. package/board/permissions.mjs +2 -2
  14. package/board/public/app-asks.js +2 -14
  15. package/board/public/app-banner.js +8 -3
  16. package/board/public/app-cards.js +7 -20
  17. package/board/public/app-core.js +2 -2
  18. package/board/public/app-lanes.js +7 -15
  19. package/board/public/app-reader.js +7 -3
  20. package/board/public/app-snapshot.js +1 -1
  21. package/board/public/app.js +1 -1
  22. package/board/public/attach.js +18 -2
  23. package/board/public/dialogs.js +10 -12
  24. package/board/public/hub.css +0 -3
  25. package/board/public/linear-key.js +1 -1
  26. package/board/public/sidebar.css +1 -1
  27. package/board/public/style.css +42 -36
  28. package/board/public/ui-cards.js +33 -27
  29. package/board/public/ui-enums.js +2 -2
  30. package/board/public/ui-links.js +0 -1
  31. package/board/public/ui-order.js +1 -1
  32. package/board/public/ui-questions.js +8 -6
  33. package/board/public/ui-sidebar.js +1 -1
  34. package/board/public/ui-threads.js +26 -22
  35. package/board/public/ui.js +15 -9
  36. package/board/public/view-hub-settings.js +25 -30
  37. package/board/public/view-hub.js +23 -5
  38. package/board/public/view-questions.js +1 -1
  39. package/board/public/view-settings.js +4 -4
  40. package/board/server.mjs +18 -18
  41. package/board/store.mjs +30 -27
  42. package/board/tickets.mjs +3 -3
  43. package/board/worktree.mjs +15 -15
  44. package/hub/board-repo.mjs +1 -1
  45. package/hub/github.mjs +3 -3
  46. package/hub/hub.mjs +11 -11
  47. package/hub/linear.mjs +1 -1
  48. package/hub/new-board.mjs +2 -2
  49. package/hub/pr-report.mjs +2 -2
  50. package/hub/readiness.mjs +1 -1
  51. package/hub/repos.mjs +3 -3
  52. package/hub/review-send.mjs +1 -1
  53. package/hub/schedule.mjs +1 -1
  54. package/hub/slack.mjs +1 -1
  55. package/hub/try.mjs +8 -6
  56. package/hub/views.mjs +1 -1
  57. package/package.json +1 -1
  58. package/shared/fake-gh.mjs +19 -4
  59. package/shared/gh.mjs +4 -0
  60. package/shared/owner-check.mjs +4 -3
  61. package/shared/service.mjs +12 -12
  62. package/starter/server.mjs +3 -3
@@ -1,12 +1,27 @@
1
- var p=Object.defineProperty;var t=(s,e)=>p(s,"name",{value:e,configurable:!0});import n from"node:fs";import l from"node:path";import{PR_VIEW_FIELDS as g}from"../hub/pr-report.mjs";const o=t((s,e,r,i)=>{const a=l.join(s,e,`${String(r).replace(/[/#]/g,"__")}.json`);n.mkdirSync(l.dirname(a),{recursive:!0}),n.writeFileSync(a,JSON.stringify(i))},"write"),y=t((s,e,r)=>o(s,"pr",e,r),"writePR"),v=t((s,e,r)=>o(s,"issue-comments",e,r),"writeIssueComments"),x=t((s,e,r)=>o(s,"review-comments",e,r),"writeReviewComments"),b=t((s,e,r)=>o(s,"reviews",e,r),"writeReviews"),_=t((s,e,r)=>o(s,"merge",e,r),"writeMerge"),j=t((s,e,r)=>o(s,"ready",e,r),"writeReady"),S=t((s,e,r)=>o(s,"open",e,r),"writeOpen"),R=t((s,e)=>o(s,"","user",e),"writeUser"),O=t((s,e,r={login:e})=>o(s,"github-users",e,r),"writeGithubUser"),N=t((s,e,r)=>o(s,"collaborators",e.replace("/","__"),r),"writeCollaborators"),k=t((s,e)=>o(s,"","accounts",e),"writeAccounts"),c=t((s,e)=>{try{return n.readFileSync(l.join(s,e),"utf8").trim().split(`
2
- `).filter(Boolean).map(JSON.parse)}catch{return[]}},"jsonl"),f=t(s=>c(s,"calls.jsonl"),"calls"),$=t((s,e)=>o(s,"","rate",e),"writeRate"),q=t(s=>f(s).filter(e=>e[0]==="api"&&e[1]==="graphql").map(e=>[...e[3].matchAll(/repository\(owner: "([^"]+)", name: "([^"]+)"\) \{ pullRequest\(number: (\d+)\)/g)].map(([,r,i,a])=>`${r}/${i}#${a}`)),"queried"),E=t(s=>c(s,"tokens.jsonl"),"tokens");function J({head:s="abc1234567",failing:e=[],pending:r=0,...i}={}){return{state:"OPEN",isDraft:!1,reviewDecision:"REVIEW_REQUIRED",mergeable:"MERGEABLE",mergeStateStatus:"BLOCKED",headRefOid:s,statusCheckRollup:[...e.map(a=>({__typename:"CheckRun",name:a,status:"COMPLETED",conclusion:"FAILURE"})),...Array.from({length:r},(a,u)=>({__typename:"CheckRun",name:`ci-${u}`,status:"IN_PROGRESS",conclusion:""}))],...i}}t(J,"prView");function A(s){const e=l.join(s,"bin");return n.mkdirSync(e,{recursive:!0}),n.writeFileSync(l.join(e,"gh"),`#!${process.execPath}
1
+ var p=Object.defineProperty;var t=(r,e)=>p(r,"name",{value:e,configurable:!0});import n from"node:fs";import l from"node:path";import{PR_VIEW_FIELDS as g}from"../hub/pr-report.mjs";const a=t((r,e,s,i)=>{const o=l.join(r,e,`${String(s).replace(/[/#]/g,"__")}.json`);n.mkdirSync(l.dirname(o),{recursive:!0}),n.writeFileSync(o,JSON.stringify(i))},"write"),y=t((r,e,s)=>a(r,"pr",e,s),"writePR"),x=t((r,e,s)=>a(r,"issue-comments",e,s),"writeIssueComments"),b=t((r,e,s)=>a(r,"review-comments",e,s),"writeReviewComments"),v=t((r,e,s)=>a(r,"reviews",e,s),"writeReviews"),_=t((r,e,s)=>a(r,"merge",e,s),"writeMerge"),S=t((r,e,s)=>a(r,"ready",e,s),"writeReady"),j=t((r,e,s)=>a(r,"open",e,s),"writeOpen"),R=t((r,e)=>a(r,"","user",e),"writeUser"),O=t((r,e,s={login:e})=>a(r,"github-users",e,s),"writeGithubUser"),q=t((r,e,s)=>a(r,"collaborators",e.replace("/","__"),s),"writeCollaborators"),N=t((r,e)=>a(r,"","accounts",e),"writeAccounts"),c=t((r,e)=>{try{return n.readFileSync(l.join(r,e),"utf8").trim().split(`
2
+ `).filter(Boolean).map(JSON.parse)}catch{return[]}},"jsonl"),f=t(r=>c(r,"calls.jsonl"),"calls"),k=t((r,e)=>a(r,"","rate",e),"writeRate"),A=t((r,e)=>a(r,"","limit",e),"writeLimit"),E=t(r=>f(r).filter(e=>e[0]==="api"&&e[1]==="graphql").map(e=>[...e[3].matchAll(/repository\(owner: "([^"]+)", name: "([^"]+)"\) \{ pullRequest\(number: (\d+)\)/g)].map(([,s,i,o])=>`${s}/${i}#${o}`)),"queried"),J=t(r=>c(r,"tokens.jsonl"),"tokens");function P({head:r="abc1234567",failing:e=[],pending:s=0,...i}={}){return{state:"OPEN",isDraft:!1,reviewDecision:"REVIEW_REQUIRED",mergeable:"MERGEABLE",mergeStateStatus:"BLOCKED",headRefOid:r,statusCheckRollup:[...e.map(o=>({__typename:"CheckRun",name:o,status:"COMPLETED",conclusion:"FAILURE"})),...Array.from({length:s},(o,u)=>({__typename:"CheckRun",name:`ci-${u}`,status:"IN_PROGRESS",conclusion:""}))],...i}}t(P,"prView");function I(r){const e=l.join(r,"bin");return n.mkdirSync(e,{recursive:!0}),n.writeFileSync(l.join(e,"gh"),`#!${process.execPath}
3
3
  const fs = require('node:fs');
4
4
  const path = require('node:path');
5
- const dir = ${JSON.stringify(s)};
5
+ const dir = ${JSON.stringify(r)};
6
6
  const args = process.argv.slice(2);
7
7
  fs.appendFileSync(path.join(dir, 'calls.jsonl'), JSON.stringify(args) + '\\n');
8
8
  fs.appendFileSync(path.join(dir, 'tokens.jsonl'), JSON.stringify({ args, token: process.env.GH_TOKEN || null }) + '\\n');
9
9
  const fail = () => { console.error('no fixture for: ' + args.join(' ')); process.exit(1); };
10
+ let limit = null;
11
+ try { limit = JSON.parse(fs.readFileSync(path.join(dir, 'limit.json'), 'utf8')); } catch {}
12
+ if (args[0] === 'api' && args[1] === 'rate_limit') {
13
+ const budget = (b) => (limit?.[b] ? { limit: 5000, remaining: 0, reset: Math.floor(Date.parse(limit[b]) / 1000) } : { limit: 5000, remaining: 4990, reset: Math.floor(Date.now() / 1000) + 3600 });
14
+ console.log(JSON.stringify({ resources: { core: budget('core'), graphql: budget('graphql') } }));
15
+ process.exit(0);
16
+ }
17
+ const spends = args[0] === 'api' ? (args.slice(1).find((a) => !a.startsWith('-')) === 'graphql' ? 'graphql' : 'core')
18
+ : ['pr', 'issue', 'repo'].includes(args[0]) ? 'graphql' : args[0] === 'run' || (args[0] === 'auth' && args[1] !== 'token') ? 'core' : null;
19
+ if (spends && limit?.secondary) { console.error('gh: You have exceeded a secondary rate limit. Please wait a few minutes before you try again. (HTTP 403)'); process.exit(1); }
20
+ if (spends && limit?.[spends]) {
21
+ if (args[1] === 'graphql') console.log(JSON.stringify({ errors: [{ type: 'RATE_LIMITED', message: 'API rate limit already exceeded for user ID 1.' }] }));
22
+ console.error(spends === 'graphql' ? 'GraphQL: API rate limit already exceeded for user ID 1.' : 'gh: API rate limit exceeded for user ID 1. (HTTP 403)');
23
+ process.exit(1);
24
+ }
10
25
  // The repo's own fixture for PR n, else the one for n in any repo.
11
26
  const fixture = (folder, repo, n) => {
12
27
  const own = path.join(dir, folder, repo.replace('/', '__') + '__' + n + '.json');
@@ -115,4 +130,4 @@ if (list) {
115
130
  try { if (JSON.parse(value).fail) fail(); } catch {}
116
131
  console.log(value);
117
132
  }
118
- `,{mode:493}),e}t(A,"fakeGh");export{f as calls,A as fakeGh,J as prView,q as queried,E as tokens,k as writeAccounts,N as writeCollaborators,O as writeGithubUser,v as writeIssueComments,_ as writeMerge,S as writeOpen,y as writePR,$ as writeRate,j as writeReady,x as writeReviewComments,b as writeReviews,R as writeUser};
133
+ `,{mode:493}),e}t(I,"fakeGh");export{f as calls,I as fakeGh,P as prView,E as queried,J as tokens,N as writeAccounts,q as writeCollaborators,O as writeGithubUser,x as writeIssueComments,A as writeLimit,_ as writeMerge,j as writeOpen,y as writePR,k as writeRate,S as writeReady,b as writeReviewComments,v as writeReviews,R as writeUser};
package/shared/gh.mjs ADDED
@@ -0,0 +1,4 @@
1
+ var q=Object.defineProperty;var u=(t,e)=>q(t,"name",{value:e,configurable:!0});import f from"node:fs";import F from"node:os";import h from"node:path";import{execFile as N,spawnSync as v}from"node:child_process";import{clock as I}from"../hub/schedule.mjs";const p="primary",g=u((t=F.homedir())=>h.join(t,".claude","kickflip","gh-limit.json"),"gateFile");function y(t){const[e,n]=t;if(e==="auth")return n==="token"||n==="git-credential"?null:"core";if(e==="api"){const r=t.slice(1).find(s=>!s.startsWith("-"));return r==="graphql"?"graphql":r==="rate_limit"?null:"core"}return e==="run"?"core":["pr","issue","repo"].includes(e)?"graphql":null}u(y,"budgetOf");const S=u(t=>{try{return JSON.parse(f.readFileSync(t,"utf8"))}catch{return{}}},"readGate");function w({account:t=p,budget:e,now:n=Date.now(),file:r=g()}={}){return Object.entries(S(r)[t]||{}).filter(([o,c])=>(!e||o===e)&&Date.parse(c)>n).map(([,o])=>o).sort().at(-1)||null}u(w,"limitedUntil");const b=u((t,e=p,n=new Date)=>`GitHub rate limited${e===p?"":` for ${e}`}, resets at ${I(new Date(t),n)}`,"limitWords"),O=u(t=>/API rate limit (already )?exceeded/i.test(t)&&!/secondary rate limit/i.test(t),"hitLimit");function x(t,e,n){let r;try{r=JSON.parse(n).resources}catch{return!1}const s=["core","graphql"].filter(a=>r?.[a]?.remaining===0&&Number(r[a].reset)*1e3>Date.now());if(!s.length)return!1;const o=S(t),c=Date.now();for(const[a,l]of Object.entries(o)){for(const[d,m]of Object.entries(l))Date.parse(m)>c||delete l[d];Object.keys(l).length||delete o[a]}o[e]={...o[e],...Object.fromEntries(s.map(a=>[a,new Date(r[a].reset*1e3).toISOString()]))},f.mkdirSync(h.dirname(t),{recursive:!0});const i=`${t}.${process.pid}.tmp`;return f.writeFileSync(i,JSON.stringify(o,null,2)+`
2
+ `),f.renameSync(i,t),!0}u(x,"record");const $=u((t,e,n)=>{const r=y(t),s=r&&w({account:e,budget:r,file:n});return s?{status:1,stdout:"",stderr:b(s,e),limited:s}:null},"refusal"),D=u((t,e,n,r)=>{const s=w({account:n,budget:y(e),file:r});return s?{...t,stderr:b(s,n),limited:s}:t},"gated");async function W(t,{env:e=process.env,account:n=p,file:r=g(),...s}={}){const o=$(t,n,r);if(o)return o;const c=u(l=>new Promise(d=>N("gh",l,{...s,env:e},(m,j,k)=>d({status:m?typeof m.code=="number"?m.code:1:0,stdout:String(j??""),stderr:String(k||(m?m.message:""))}))),"run"),i=await c(t);if(i.status===0||!O(`${i.stderr}
3
+ ${i.stdout}`))return i;const a=await c(["api","rate_limit"]);return a.status===0&&x(r,n,a.stdout)?D(i,t,n,r):i}u(W,"gh");function E(t,{env:e=process.env,account:n=p,file:r=g(),...s}={}){const o=$(t,n,r);if(o)return o;const c=u(l=>{const d=v("gh",l,{encoding:"utf8",...s,env:e});return{status:d.status??1,stdout:d.stdout||"",stderr:d.stderr||d.error?.message||""}},"run"),i=c(t);if(i.status===0||!O(`${i.stderr}
4
+ ${i.stdout}`))return i;const a=c(["api","rate_limit"]);return a.status===0&&x(r,n,a.stdout)?D(i,t,n,r):i}u(E,"ghSync");export{p as PRIMARY,y as budgetOf,g as gateFile,W as gh,E as ghSync,b as limitWords,w as limitedUntil};
@@ -1,3 +1,4 @@
1
- var h=Object.defineProperty;var r=(t,e)=>h(t,"name",{value:e,configurable:!0});import{execFile as m}from"node:child_process";const k=/^[\w.-]+\/[\w.-]+$/,A=/^(@[\w.-]+\/)?[\w.][\w.-]*$/,S=/^[\w.-]+(\/[\w.-]+)*$/,b=/^[\w-]+(\.[\w-]+)*$/,u=r(t=>{throw new Error(t)},"bad"),d=r(t=>k.test(t||"")?t:u(`"${t??""}" isn't a GitHub repo such as owner/name`),"repoOf"),p=r((t,e)=>t&&!t.startsWith("-")?t:u(`${e} is missing`),"plain"),l={tag:{usage:"tag <owner/repo> <pattern>",args:r(([t,e,...s])=>s.length?u("tag takes a repo and one pattern"):[d(t),p(e,"the tag pattern")],"args"),words:r(([t,e])=>`a ${e} tag on ${t}`,"words"),async run([t,e],{exec:s}){const n=await s("git",["ls-remote","--tags",`https://github.com/${t}.git`]),a=j(e),i=[...new Set(n.split(`
2
- `).map(c=>c.split(" ")[1]?.replace(/^refs\/tags\//,"").replace(/\^\{\}$/,"")).filter(Boolean))].filter(c=>a.test(c)).sort(g).at(-1);return i?{passed:!0,found:`tag ${i} is on ${t}`}:{passed:!1}}},npm:{usage:"npm <package> <version-or-range>",args:r(([t,e,...s])=>s.length?u("npm takes a package and one version or range"):[A.test(t||"")?t:u(`"${t??""}" isn't an npm package name`),p(e,"the version or range")],"args"),words:r(([t,e])=>`${t}@${e} on npm`,"words"),async run([t,e],{exec:s}){const n=(await s("npm",["view",`${t}@${e}`,"version","--json"]).catch(i=>{if(/No match found for version/.test(`${i.stdout??""}${i.stderr??""}`))return"";throw i})).trim(),o=(n?[JSON.parse(n)].flat():[]).sort(g).at(-1);return o?{passed:!0,found:`${t}@${o} is on npm`}:{passed:!1}}},run:{usage:"run <owner/repo> <workflow>",args:r(([t,...e])=>[d(t),p(e.join(" "),"the workflow")],"args"),words:r(([t,e])=>`a passing ${e} run on ${t}`,"words"),async run([t,e],{exec:s,since:n}){const a=await s("gh",["run","list","--repo",t,"--workflow",e,"--status","success","--limit","20","--json","createdAt,url"]),o=JSON.parse(a||"[]").filter(i=>String(i.createdAt)>=n).sort((i,c)=>String(i.createdAt).localeCompare(String(c.createdAt))).at(-1);return o?{passed:!0,found:`${e} passed on ${t} (${o.url})`}:{passed:!1}}},gh:{usage:"gh <owner/repo> <api-path> <field>=<value>",args([t,e,s,...n]){n.length&&u("gh takes a repo, an API path and one field=value"),(!S.test(e||"")||e.split("/").includes(".."))&&u(`"${e??""}" isn't an API path under the repo, such as actions/permissions`);const[a,...o]=String(s??"").split("=");return(!b.test(a)||!o.length)&&u(`"${s??""}" isn't a field=value, such as enabled=true`),[d(t),e,s]},words:r(([t,e,s])=>`${e} reads ${s} on ${t}`,"words"),async run([t,e,s],{exec:n}){const[a,...o]=s.split("="),i=o.join("="),c=a.split(".").reduce((f,w)=>f?.[w],JSON.parse(await n("gh",["api",`repos/${t}/${e}`])));return c!==void 0&&String(c)===i?{passed:!0,found:`${e} reads ${s} on ${t}`}:{passed:!1}}},url:{usage:"url <http(s) address> [<status> | <text>]",args([t,...e]){let s;try{s=new URL(t)}catch{u(`"${t??""}" isn't a web address`)}return["http:","https:"].includes(s.protocol)||u("url takes an http or https address"),e.length?[t,e.join(" ")]:[t]},words:r(([t,e])=>e===void 0||/^\d{3}$/.test(e)?`${t} answers ${e??200}`:`${t} shows "${e}"`,"words"),async run([t,e],{fetchImpl:s}){const n=e!==void 0&&/^\d{3}$/.test(e)?Number(e):200,a=await s(t,{redirect:"follow",signal:AbortSignal.timeout(2e4)}),o=await a.text();return a.status!==n?{passed:!1}:n===200&&e!==void 0&&!/^\d{3}$/.test(e)?o.includes(e)?{passed:!0,found:`${t} shows "${e}"`}:{passed:!1}:{passed:!0,found:`${t} answered ${n}`}}}},$=Object.values(l).map(t=>t.usage);function E(t){const[e,...s]=String(t??"").trim().split(/\s+/).filter(Boolean);e||u(`--check takes "<name> <args>": ${$.join(", ")}`);const n=Object.hasOwn(l,e)?l[e]:u(`--check has no check "${e}"; it takes ${$.join(", ")}`);try{return{name:e,args:n.args(s)}}catch(a){return u(`--check ${n.usage}: ${a.message}`)}}r(E,"parseCheck");const C=r(({name:t,args:e})=>[t,...e].join(" "),"checkText"),I=r(({name:t,args:e})=>l[t].words(e),"checkWords");function N({name:t,args:e},{since:s,exec:n=v,fetchImpl:a=fetch}={}){return l[t].run(e,{since:s,exec:n,fetchImpl:a})}r(N,"runCheck");function v(t,e){return new Promise((s,n)=>{m(t,e,{timeout:6e4,maxBuffer:16*1024*1024,env:{...process.env,GIT_TERMINAL_PROMPT:"0"}},(a,o,i)=>{if(!a)return s(o);const c=String(i||"").trim().split(`
3
- `).find(Boolean)?.replace(/^npm error /,"");n(Object.assign(new Error(`${t} ${e[0]} failed: ${c||a.message}`),{stdout:o,stderr:i}))})})}r(v,"execArgv");function j(t){return new RegExp(`^${t.split("").map(e=>e==="*"?".*":e==="?"?".":e.replace(/[.+^${}()|[\]\\]/g,"\\$&")).join("")}$`)}r(j,"globOf");function g(t,e){return t.localeCompare(e,"en",{numeric:!0})}r(g,"byVersion");export{$ as CHECK_USAGE,C as checkText,I as checkWords,E as parseCheck,N as runCheck};
1
+ var h=Object.defineProperty;var r=(t,s)=>h(t,"name",{value:s,configurable:!0});import{execFile as m}from"node:child_process";import{gh as k}from"./gh.mjs";const b=/^[\w.-]+\/[\w.-]+$/,A=/^(@[\w.-]+\/)?[\w.][\w.-]*$/,S=/^[\w.-]+(\/[\w.-]+)*$/,j=/^[\w-]+(\.[\w-]+)*$/,u=r(t=>{throw new Error(t)},"bad"),d=r(t=>b.test(t||"")?t:u(`"${t??""}" isn't a GitHub repo such as owner/name`),"repoOf"),f=r((t,s)=>t&&!t.startsWith("-")?t:u(`${s} is missing`),"plain"),l={tag:{usage:"tag <owner/repo> <pattern>",args:r(([t,s,...e])=>e.length?u("tag takes a repo and one pattern"):[d(t),f(s,"the tag pattern")],"args"),words:r(([t,s])=>`a ${s} tag on ${t}`,"words"),async run([t,s],{exec:e}){const n=await e("git",["ls-remote","--tags",`https://github.com/${t}.git`]),a=O(s),i=[...new Set(n.split(`
2
+ `).map(c=>c.split(" ")[1]?.replace(/^refs\/tags\//,"").replace(/\^\{\}$/,"")).filter(Boolean))].filter(c=>a.test(c)).sort(p).at(-1);return i?{passed:!0,found:`tag ${i} is on ${t}`}:{passed:!1}}},npm:{usage:"npm <package> <version-or-range>",args:r(([t,s,...e])=>e.length?u("npm takes a package and one version or range"):[A.test(t||"")?t:u(`"${t??""}" isn't an npm package name`),f(s,"the version or range")],"args"),words:r(([t,s])=>`${t}@${s} on npm`,"words"),async run([t,s],{exec:e}){const n=(await e("npm",["view",`${t}@${s}`,"version","--json"]).catch(i=>{if(/No match found for version/.test(`${i.stdout??""}${i.stderr??""}`))return"";throw i})).trim(),o=(n?[JSON.parse(n)].flat():[]).sort(p).at(-1);return o?{passed:!0,found:`${t}@${o} is on npm`}:{passed:!1}}},run:{usage:"run <owner/repo> <workflow>",args:r(([t,...s])=>[d(t),f(s.join(" "),"the workflow")],"args"),words:r(([t,s])=>`a passing ${s} run on ${t}`,"words"),async run([t,s],{exec:e,since:n}){const a=await e("gh",["run","list","--repo",t,"--workflow",s,"--status","success","--limit","20","--json","createdAt,url"]),o=JSON.parse(a||"[]").filter(i=>String(i.createdAt)>=n).sort((i,c)=>String(i.createdAt).localeCompare(String(c.createdAt))).at(-1);return o?{passed:!0,found:`${s} passed on ${t} (${o.url})`}:{passed:!1}}},gh:{usage:"gh <owner/repo> <api-path> <field>=<value>",args([t,s,e,...n]){n.length&&u("gh takes a repo, an API path and one field=value"),(!S.test(s||"")||s.split("/").includes(".."))&&u(`"${s??""}" isn't an API path under the repo, such as actions/permissions`);const[a,...o]=String(e??"").split("=");return(!j.test(a)||!o.length)&&u(`"${e??""}" isn't a field=value, such as enabled=true`),[d(t),s,e]},words:r(([t,s,e])=>`${s} reads ${e} on ${t}`,"words"),async run([t,s,e],{exec:n}){const[a,...o]=e.split("="),i=o.join("="),c=a.split(".").reduce((g,w)=>g?.[w],JSON.parse(await n("gh",["api",`repos/${t}/${s}`])));return c!==void 0&&String(c)===i?{passed:!0,found:`${s} reads ${e} on ${t}`}:{passed:!1}}},url:{usage:"url <http(s) address> [<status> | <text>]",args([t,...s]){let e;try{e=new URL(t)}catch{u(`"${t??""}" isn't a web address`)}return["http:","https:"].includes(e.protocol)||u("url takes an http or https address"),s.length?[t,s.join(" ")]:[t]},words:r(([t,s])=>s===void 0||/^\d{3}$/.test(s)?`${t} answers ${s??200}`:`${t} shows "${s}"`,"words"),async run([t,s],{fetchImpl:e}){const n=s!==void 0&&/^\d{3}$/.test(s)?Number(s):200,a=await e(t,{redirect:"follow",signal:AbortSignal.timeout(2e4)}),o=await a.text();return a.status!==n?{passed:!1}:n===200&&s!==void 0&&!/^\d{3}$/.test(s)?o.includes(s)?{passed:!0,found:`${t} shows "${s}"`}:{passed:!1}:{passed:!0,found:`${t} answered ${n}`}}}},$=Object.values(l).map(t=>t.usage);function I(t){const[s,...e]=String(t??"").trim().split(/\s+/).filter(Boolean);s||u(`--check takes "<name> <args>": ${$.join(", ")}`);const n=Object.hasOwn(l,s)?l[s]:u(`--check has no check "${s}"; it takes ${$.join(", ")}`);try{return{name:s,args:n.args(e)}}catch(a){return u(`--check ${n.usage}: ${a.message}`)}}r(I,"parseCheck");const N=r(({name:t,args:s})=>[t,...s].join(" "),"checkText"),_=r(({name:t,args:s})=>l[t].words(s),"checkWords");function B({name:t,args:s},{since:e,exec:n=v,fetchImpl:a=fetch}={}){return l[t].run(s,{since:e,exec:n,fetchImpl:a})}r(B,"runCheck");async function v(t,s){if(t==="gh"){const e=await k(s,{timeout:6e4,maxBuffer:16777216});if(e.status===0)return e.stdout;throw Object.assign(new Error(`gh ${s[0]} failed: ${e.stderr.trim().split(`
3
+ `).find(Boolean)||`exit ${e.status}`}`),{stdout:e.stdout,stderr:e.stderr})}return new Promise((e,n)=>{m(t,s,{timeout:6e4,maxBuffer:16*1024*1024,env:{...process.env,GIT_TERMINAL_PROMPT:"0"}},(a,o,i)=>{if(!a)return e(o);const c=String(i||"").trim().split(`
4
+ `).find(Boolean)?.replace(/^npm error /,"");n(Object.assign(new Error(`${t} ${s[0]} failed: ${c||a.message}`),{stdout:o,stderr:i}))})})}r(v,"execArgv");function O(t){return new RegExp(`^${t.split("").map(s=>s==="*"?".*":s==="?"?".":s.replace(/[.+^${}()|[\]\\]/g,"\\$&")).join("")}$`)}r(O,"globOf");function p(t,s){return t.localeCompare(s,"en",{numeric:!0})}r(p,"byVersion");export{$ as CHECK_USAGE,N as checkText,_ as checkWords,I as parseCheck,B as runCheck};
@@ -1,33 +1,33 @@
1
- var S=Object.defineProperty;var n=(i,u)=>S(i,"name",{value:u,configurable:!0});import p from"node:fs";import v from"node:os";import $ from"node:path";import{spawnSync as A}from"node:child_process";const d="local.claude.kickflip-hub",m="kickflip-hub.service";function T(i,u,{env:g=process.env,timeout:o=1e4}={}){const r=A(i,u,{encoding:"utf8",env:g,timeout:o});return{status:r.status??1,stdout:r.stdout||"",error:(r.error?.message||r.stderr||`exit ${r.status}`).trim().split(`
2
- `)[0]}}n(T,"runCommand");function D({platform:i=process.platform,home:u=v.homedir(),uid:g=process.getuid?.(),env:o=process.env,run:r=T}={}){const a=n((t,s,c={})=>{try{return r(t,s,{env:o,...c})}catch(l){return{status:1,stdout:"",error:l.message}}},"exec");if(i==="darwin")return E({home:u,uid:g,exec:a});if(i==="linux")return L({home:u,uid:g,exec:a});const e=`kickflip keeps the hub running with launchd on macOS or systemd on Linux, not on ${i}`;return{manager:null,item:"hub service",file:null,id:null,restartCommand:"node hub/hub.mjs from the kickflip folder",definition:n(()=>null,"definition"),install:n(async()=>{throw new Error(e)},"install"),restart:n(()=>e,"restart"),remove:n(()=>{},"remove"),status:n(()=>({installed:!1,running:!1,detail:e}),"status"),notes:n(()=>[],"notes")}}n(D,"hubService");const b=12e4;function E({home:i,uid:u,exec:g}){const o=$.join(i,"Library","LaunchAgents"),r=`gui/${u}`,a=$.join(o,`${d}.plist`),e=n((t,s)=>g("launchctl",t,s),"launchctl");return{manager:"launchd",item:"hub login item",file:a,id:`${r}/${d}`,restartCommand:`launchctl kickstart -k gui/$(id -u)/${d}`,definition({node:t,entry:s,state:c,port:l,log:h}){const y=n(w=>String(w).replaceAll("&","&amp;").replaceAll("<","&lt;").replaceAll(">","&gt;"),"xml");return`<?xml version="1.0" encoding="UTF-8"?>
1
+ var S=Object.defineProperty;var n=(s,l)=>S(s,"name",{value:l,configurable:!0});import p from"node:fs";import v from"node:os";import $ from"node:path";import{spawnSync as A}from"node:child_process";import{ghSync as T}from"./gh.mjs";const m="local.claude.kickflip-hub",g="kickflip-hub.service";function E(s,l,{env:c=process.env,timeout:o=1e4}={}){const r=s==="gh"?T(l,{env:c,timeout:o}):A(s,l,{encoding:"utf8",env:c,timeout:o});return{status:r.status??1,stdout:r.stdout||"",error:(r.error?.message||r.stderr||`exit ${r.status}`).trim().split(`
2
+ `)[0]}}n(E,"runCommand");function U({platform:s=process.platform,home:l=v.homedir(),uid:c=process.getuid?.(),env:o=process.env,run:r=E}={}){const a=n((t,i,d={})=>{try{return r(t,i,{env:o,...d})}catch(u){return{status:1,stdout:"",error:u.message}}},"exec");if(s==="darwin")return L({home:l,uid:c,exec:a});if(s==="linux")return x({home:l,uid:c,exec:a});const e=`kickflip keeps the hub running with launchd on macOS or systemd on Linux, not on ${s}`;return{manager:null,item:"hub service",file:null,id:null,restartCommand:"node hub/hub.mjs from the kickflip folder",definition:n(()=>null,"definition"),install:n(async()=>{throw new Error(e)},"install"),restart:n(()=>e,"restart"),remove:n(()=>{},"remove"),status:n(()=>({installed:!1,running:!1,detail:e}),"status"),notes:n(()=>[],"notes")}}n(U,"hubService");const b=12e4;function L({home:s,uid:l,exec:c}){const o=$.join(s,"Library","LaunchAgents"),r=`gui/${l}`,a=$.join(o,`${m}.plist`),e=n((t,i)=>c("launchctl",t,i),"launchctl");return{manager:"launchd",item:"hub login item",file:a,id:`${r}/${m}`,restartCommand:`launchctl kickstart -k gui/$(id -u)/${m}`,definition({node:t,entry:i,state:d,port:u,log:h}){const y=n(w=>String(w).replaceAll("&","&amp;").replaceAll("<","&lt;").replaceAll(">","&gt;"),"xml");return`<?xml version="1.0" encoding="UTF-8"?>
3
3
  <!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
4
4
  <plist version="1.0">
5
5
  <dict>
6
- <key>Label</key><string>${d}</string>
6
+ <key>Label</key><string>${m}</string>
7
7
  <key>ProgramArguments</key>
8
- <array><string>${y(t)}</string><string>${y(s)}</string></array>
9
- <key>WorkingDirectory</key><string>${y(c)}</string>
8
+ <array><string>${y(t)}</string><string>${y(i)}</string></array>
9
+ <key>WorkingDirectory</key><string>${y(d)}</string>
10
10
  <key>EnvironmentVariables</key>
11
- <dict><key>PATH</key><string>${y($.dirname(t))}:/opt/homebrew/bin:/usr/local/bin:/usr/bin:/bin:/usr/sbin:/sbin</string><key>KICKFLIP_HUB_PORT</key><string>${l}</string></dict>
11
+ <dict><key>PATH</key><string>${y($.dirname(t))}:/opt/homebrew/bin:/usr/local/bin:/usr/bin:/bin:/usr/sbin:/sbin</string><key>KICKFLIP_HUB_PORT</key><string>${u}</string></dict>
12
12
  <key>RunAtLoad</key><true/>
13
13
  <key>KeepAlive</key><true/>
14
14
  <key>StandardOutPath</key><string>${y(h)}</string>
15
15
  <key>StandardErrorPath</key><string>${y(h)}</string>
16
16
  </dict>
17
17
  </plist>
18
- `},async install(t,{wait:s=n(l=>new Promise(h=>setTimeout(h,l)),"wait"),dryRun:c=!1}={}){if(c)return[`would write ${a} and start ${r}/${d}`];p.mkdirSync(o,{recursive:!0}),p.writeFileSync(a,t),e(["bootout",`${r}/${d}`]),await s(500);const l=e(["bootstrap",r,a]);if(l.status===0)return[`started ${r}/${d} from ${a}`];throw new Error(l.error)},restart(){const t=e(["kickstart","-k",`${r}/${d}`],{timeout:b});return t.status===0?null:t.error},remove(){e(["bootout",`${r}/${d}`]),p.rmSync(a,{force:!0})},status(){const t=e(["print",`${r}/${d}`]),s=p.existsSync(a),c=s&&t.status===0&&/\bstate = running\b/.test(t.stdout||"");return{installed:s,running:c,detail:c?"running":s?"not running":"not installed"}},notes:n(()=>[],"notes")}}n(E,"launchd");const k=n(i=>String(i).replaceAll("%","%%").replaceAll("$","$$$$").replace(/[\r\n]/g," "),"unitValue"),f=n(i=>`"${k(i).replaceAll("\\","\\\\").replaceAll('"','\\"')}"`,"execWord");function L({home:i,uid:u,exec:g}){const o=$.join(i,".config","systemd","user",m),r=n((e,t)=>g("systemctl",["--user",...e],t),"systemctl"),a=n(e=>{const t=r(e);if(t.status!==0)throw new Error(`systemctl --user ${e.join(" ")}: ${t.error}`)},"step");return{manager:"systemd",item:"hub service",file:o,id:m,restartCommand:`systemctl --user restart ${m}`,definition({node:e,entry:t,state:s,port:c,log:l}){return`[Unit]
18
+ `},async install(t,{wait:i=n(u=>new Promise(h=>setTimeout(h,u)),"wait"),dryRun:d=!1}={}){if(d)return[`would write ${a} and start ${r}/${m}`];p.mkdirSync(o,{recursive:!0}),p.writeFileSync(a,t),e(["bootout",`${r}/${m}`]),await i(500);const u=e(["bootstrap",r,a]);if(u.status===0)return[`started ${r}/${m} from ${a}`];throw new Error(u.error)},restart(){const t=e(["kickstart","-k",`${r}/${m}`],{timeout:b});return t.status===0?null:t.error},remove(){e(["bootout",`${r}/${m}`]),p.rmSync(a,{force:!0})},status(){const t=e(["print",`${r}/${m}`]),i=p.existsSync(a),d=i&&t.status===0&&/\bstate = running\b/.test(t.stdout||"");return{installed:i,running:d,detail:d?"running":i?"not running":"not installed"}},notes:n(()=>[],"notes")}}n(L,"launchd");const k=n(s=>String(s).replaceAll("%","%%").replaceAll("$","$$$$").replace(/[\r\n]/g," "),"unitValue"),f=n(s=>`"${k(s).replaceAll("\\","\\\\").replaceAll('"','\\"')}"`,"execWord");function x({home:s,uid:l,exec:c}){const o=$.join(s,".config","systemd","user",g),r=n((e,t)=>c("systemctl",["--user",...e],t),"systemctl"),a=n(e=>{const t=r(e);if(t.status!==0)throw new Error(`systemctl --user ${e.join(" ")}: ${t.error}`)},"step");return{manager:"systemd",item:"hub service",file:o,id:g,restartCommand:`systemctl --user restart ${g}`,definition({node:e,entry:t,state:i,port:d,log:u}){return`[Unit]
19
19
  Description=kickflip hub
20
20
 
21
21
  [Service]
22
22
  ExecStart=${f(e)} ${f(t)}
23
- WorkingDirectory=${k(s)}
23
+ WorkingDirectory=${k(i)}
24
24
  Environment=${f(`PATH=${$.dirname(e)}:/usr/local/bin:/usr/bin:/bin:/usr/sbin:/sbin`)}
25
- Environment=KICKFLIP_HUB_PORT=${c}
25
+ Environment=KICKFLIP_HUB_PORT=${d}
26
26
  Restart=always
27
27
  RestartSec=2
28
- StandardOutput=append:${k(l)}
29
- StandardError=append:${k(l)}
28
+ StandardOutput=append:${k(u)}
29
+ StandardError=append:${k(u)}
30
30
 
31
31
  [Install]
32
32
  WantedBy=default.target
33
- `},async install(e,{dryRun:t=!1}={}){return t?[`would write ${o}, enable and start ${m}`]:(p.mkdirSync($.dirname(o),{recursive:!0}),p.writeFileSync(o,e),a(["daemon-reload"]),a(["enable",m]),a(["restart",m]),[`enabled and started ${m} from ${o}`])},restart(){const e=r(["restart",m],{timeout:b});return e.status===0?null:e.error},remove(){r(["disable","--now",m]),p.rmSync(o,{force:!0}),r(["daemon-reload"])},status(){const e=r(["is-active",m]),t=p.existsSync(o),s=t&&e.status===0&&(e.stdout||"").trim()==="active";return{installed:t,running:s,detail:s?"running":t?"not running":"not installed"}},notes(){const e=g("loginctl",["show-user",String(u),"--property=Linger"]),t=e.status===0&&/^Linger=yes$/m.test(e.stdout||"");return[{item:"hub without a login",state:t?"ok":"optional",detail:t?"the user lingers, so the hub runs with nobody logged in":"the hub stops when your last login session ends",fix:'sudo loginctl enable-linger "$USER"'},{item:"macOS-only parts",state:"note",detail:"The owner reads away from the host, since presence comes from macOS ioreg, so only a page tap makes the hub check every minute"}]}}}n(L,"systemd");export{d as LOGIN_LABEL,m as UNIT,D as hubService,T as runCommand};
33
+ `},async install(e,{dryRun:t=!1}={}){return t?[`would write ${o}, enable and start ${g}`]:(p.mkdirSync($.dirname(o),{recursive:!0}),p.writeFileSync(o,e),a(["daemon-reload"]),a(["enable",g]),a(["restart",g]),[`enabled and started ${g} from ${o}`])},restart(){const e=r(["restart",g],{timeout:b});return e.status===0?null:e.error},remove(){r(["disable","--now",g]),p.rmSync(o,{force:!0}),r(["daemon-reload"])},status(){const e=r(["is-active",g]),t=p.existsSync(o),i=t&&e.status===0&&(e.stdout||"").trim()==="active";return{installed:t,running:i,detail:i?"running":t?"not running":"not installed"}},notes(){const e=c("loginctl",["show-user",String(l),"--property=Linger"]),t=e.status===0&&/^Linger=yes$/m.test(e.stdout||"");return[{item:"hub without a login",state:t?"ok":"optional",detail:t?"the user lingers, so the hub runs with nobody logged in":"the hub stops when your last login session ends",fix:'sudo loginctl enable-linger "$USER"'},{item:"macOS-only parts",state:"note",detail:"The owner reads away from the host, since presence comes from macOS ioreg, so only a page tap makes the hub check every minute"}]}}}n(x,"systemd");export{m as LOGIN_LABEL,g as UNIT,U as hubService,E as runCommand};
@@ -1,4 +1,4 @@
1
1
  #!/usr/bin/env node
2
- var N=Object.defineProperty;var f=(e,n)=>N(e,"name",{value:n,configurable:!0});import b from"node:http";import r from"node:fs/promises";import i from"node:path";import{fileURLToPath as k}from"node:url";import{crossSite as H,isJSON as P}from"./cross-site.mjs";const j=i.dirname(k(import.meta.url)),u=i.join(j,"public"),g=i.join(j,"data"),y=i.join(g,"events.jsonl"),S=i.join(g,"replies"),T=i.join(g,"needs.json"),x=Number(process.env.PORT||4600),l=(process.env.PREFIX||"").replace(/\/+$/,""),v={".html":"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"},h=f((e,n,t)=>{e.writeHead(n,{"content-type":v[".json"],"cache-control":"no-store"}),e.end(JSON.stringify(t))},"json");async function J(e,n=64*1024){if(!P(e.headers))throw Object.assign(new Error("Send the body as application/json."),{status:403});let t="";for await(const a of e)if(t+=a,t.length>n)throw Object.assign(new Error("too large"),{status:413});return t?JSON.parse(t):{}}f(J,"readBody");async function D(e,n){let t=[];try{t=(await r.readFile(y,"utf8")).split(`
3
- `).filter(Boolean).map(a=>JSON.parse(a))}catch{}return e&&(t=t.filter(a=>a.kind===e)),t=t.reverse().slice(0,n),Promise.all(t.map(async a=>{for(const o of["html","json"]){const m=i.join(S,`${a.id}.${o}`);try{const[s,c]=await Promise.all([r.readFile(m,"utf8"),r.stat(m)]);return{...a,reply:{[o]:o==="json"?JSON.parse(s):s,at:c.mtime.toISOString()}}}catch{}}return{...a,reply:null}}))}f(D,"listEvents");const F=b.createServer(async(e,n)=>{try{if(e.method!=="GET"&&e.method!=="HEAD"&&H(e.headers)){n.writeHead(403,{"content-type":"text/plain; charset=utf-8","cache-control":"no-store"}),n.end("Requests must come from the app\u2019s own pages.");return}const t=new URL(e.url,"http://site");let a=t.pathname;if(l&&(a===l||a.startsWith(l+"/"))&&(a=a.slice(l.length)||"/"),a==="/_claude/health")return h(n,200,{ok:!0});if(a==="/_claude/needs"){const s=await r.readFile(T,"utf8").catch(p=>p.code==="ENOENT"?'{"items":[]}':Promise.reject(p)),{items:c,working:d,summary:w}=JSON.parse(s);if(!Array.isArray(c))throw new Error("data/needs.json has no items array");return h(n,200,{items:c.map(({kind:p,text:E,url:O})=>({kind:String(p||""),text:String(E||""),url:String(O||"")})),...Number.isInteger(d)&&d>=0?{working:d}:{},...typeof w=="string"?{summary:w}:{}})}if(a==="/_claude/events"&&e.method==="POST"){const s=await J(e),c=String(s.kind||"message").slice(0,64);delete s.kind;const d={id:"e"+Date.now().toString(36)+Math.random().toString(36).slice(2,5),ts:new Date().toISOString(),kind:c,data:s};return await r.mkdir(S,{recursive:!0}),await r.appendFile(y,JSON.stringify(d)+`
4
- `),h(n,200,{id:d.id})}if(a==="/_claude/events"&&e.method==="GET")return h(n,200,await D(t.searchParams.get("kind"),Math.min(500,Number(t.searchParams.get("limit")||100))));if(e.method!=="GET"&&e.method!=="HEAD"){n.writeHead(405).end();return}let o=i.resolve(u,"."+decodeURIComponent(a));if(o!==u&&!o.startsWith(u+i.sep)){n.writeHead(404).end();return}(await r.stat(o).catch(()=>null))?.isDirectory()&&(o=i.join(o,"index.html"));const m=await r.readFile(o);n.writeHead(200,{"content-type":v[i.extname(o).toLowerCase()]||"application/octet-stream","cache-control":"no-store"}),n.end(e.method==="HEAD"?void 0:m)}catch(t){n.headersSent||(t.status===403?n.writeHead(403,{"content-type":"text/plain; charset=utf-8","cache-control":"no-store"}).end(t.message):n.writeHead(t.status||(t.code==="ENOENT"?404:400)).end())}});F.listen(x,"127.0.0.1",()=>console.log(`site on http://127.0.0.1:${x}/ (prefix ${l||"none"})`));
2
+ var O=Object.defineProperty;var f=(e,n)=>O(e,"name",{value:n,configurable:!0});import b from"node:http";import c from"node:fs/promises";import o from"node:path";import{fileURLToPath as k}from"node:url";import{crossSite as H,isJSON as P}from"./cross-site.mjs";const S=o.dirname(k(import.meta.url)),w=o.join(S,"public"),j=o.join(S,"data"),x=o.join(j,"events.jsonl"),y=o.join(j,"replies"),T=o.join(j,"needs.json"),v=Number(process.env.PORT||4600),p=(process.env.PREFIX||"").replace(/\/+$/,""),E={".html":"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"},h=f((e,n,t)=>{e.writeHead(n,{"content-type":E[".json"],"cache-control":"no-store"}),e.end(JSON.stringify(t))},"json");async function J(e,n=64*1024){if(!P(e.headers))throw Object.assign(new Error("Send the body as application/json."),{status:403});let t="";for await(const s of e)if(t+=s,t.length>n)throw Object.assign(new Error("too large"),{status:413});return t?JSON.parse(t):{}}f(J,"readBody");async function D(e,n){let t=[];try{t=(await c.readFile(x,"utf8")).split(`
3
+ `).filter(Boolean).map(r=>JSON.parse(r))}catch{}e&&(t=t.filter(r=>r.kind===e)),t=t.reverse().slice(0,n);const s=await c.readdir(y).catch(()=>[]);return Promise.all(t.map(async r=>{const u=s.flatMap(i=>{const a=/^(.+?)(?:~(\d+))?\.(html|json)$/.exec(i);return a?.[1]===r.id?[{name:i,ext:a[3],n:Number(a[2]||1)}]:[]}).sort((i,a)=>i.n-a.n).filter((i,a,d)=>i.n!==d[a-1]?.n),l=(await Promise.all(u.map(async({name:i,ext:a})=>{const d=o.join(y,i);try{const[m,g]=await Promise.all([c.readFile(d,"utf8"),c.stat(d)]);return[{[a]:a==="json"?JSON.parse(m):m,at:g.mtime.toISOString()}]}catch{return[]}}))).flat();return{...r,reply:l.at(-1)||null,replies:l}}))}f(D,"listEvents");const F=b.createServer(async(e,n)=>{try{if(e.method!=="GET"&&e.method!=="HEAD"&&H(e.headers)){n.writeHead(403,{"content-type":"text/plain; charset=utf-8","cache-control":"no-store"}),n.end("Requests must come from the app\u2019s own pages.");return}const t=new URL(e.url,"http://site");let s=t.pathname;if(p&&(s===p||s.startsWith(p+"/"))&&(s=s.slice(p.length)||"/"),s==="/_claude/health")return h(n,200,{ok:!0});if(s==="/_claude/needs"){const l=await c.readFile(T,"utf8").catch(m=>m.code==="ENOENT"?'{"items":[]}':Promise.reject(m)),{items:i,working:a,summary:d}=JSON.parse(l);if(!Array.isArray(i))throw new Error("data/needs.json has no items array");return h(n,200,{items:i.map(({kind:m,text:g,url:N})=>({kind:String(m||""),text:String(g||""),url:String(N||"")})),...Number.isInteger(a)&&a>=0?{working:a}:{},...typeof d=="string"?{summary:d}:{}})}if(s==="/_claude/events"&&e.method==="POST"){const l=await J(e),i=String(l.kind||"message").slice(0,64);delete l.kind;const a={id:"e"+Date.now().toString(36)+Math.random().toString(36).slice(2,5),ts:new Date().toISOString(),kind:i,data:l};return await c.mkdir(y,{recursive:!0}),await c.appendFile(x,JSON.stringify(a)+`
4
+ `),h(n,200,{id:a.id})}if(s==="/_claude/events"&&e.method==="GET")return h(n,200,await D(t.searchParams.get("kind"),Math.min(500,Number(t.searchParams.get("limit")||100))));if(e.method!=="GET"&&e.method!=="HEAD"){n.writeHead(405).end();return}let r=o.resolve(w,"."+decodeURIComponent(s));if(r!==w&&!r.startsWith(w+o.sep)){n.writeHead(404).end();return}(await c.stat(r).catch(()=>null))?.isDirectory()&&(r=o.join(r,"index.html"));const u=await c.readFile(r);n.writeHead(200,{"content-type":E[o.extname(r).toLowerCase()]||"application/octet-stream","cache-control":"no-store"}),n.end(e.method==="HEAD"?void 0:u)}catch(t){n.headersSent||(t.status===403?n.writeHead(403,{"content-type":"text/plain; charset=utf-8","cache-control":"no-store"}).end(t.message):n.writeHead(t.status||(t.code==="ENOENT"?404:400)).end())}});F.listen(v,"127.0.0.1",()=>console.log(`site on http://127.0.0.1:${v}/ (prefix ${p||"none"})`));