@lovek4d/kickflip 0.3.1 → 0.4.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 (104) hide show
  1. package/CONTEXT.md +151 -115
  2. package/README.md +23 -14
  3. package/SKILL.md +37 -37
  4. package/bin/doctor.mjs +4 -4
  5. package/bin/install.mjs +3 -3
  6. package/bin/kickflip.mjs +34 -26
  7. package/bin/wait-events.mjs +2 -2
  8. package/board/arch-review.mjs +1 -1
  9. package/board/auto-review.mjs +2 -0
  10. package/board/bin/board.mjs +186 -69
  11. package/board/domain.mjs +8 -5
  12. package/board/finish.mjs +3 -0
  13. package/board/headless.mjs +3 -0
  14. package/board/on-host.mjs +1 -1
  15. package/board/permissions.mjs +2 -0
  16. package/board/public/app-arch.js +2 -2
  17. package/board/public/app-asks.js +57 -38
  18. package/board/public/app-banner.js +2 -2
  19. package/board/public/app-cards.js +156 -72
  20. package/board/public/app-core.js +2 -4
  21. package/board/public/app-header.js +12 -5
  22. package/board/public/app-lanes.js +201 -47
  23. package/board/public/app-needs.js +2 -1
  24. package/board/public/app-snapshot.js +2 -2
  25. package/board/public/app-threads.js +11 -14
  26. package/board/public/app.html +2 -1
  27. package/board/public/app.js +53 -83
  28. package/board/public/attach.js +39 -3
  29. package/board/public/dialogs.js +59 -14
  30. package/board/public/hub-kit.js +9 -6
  31. package/board/public/hub.css +5 -18
  32. package/board/public/linear-key.js +6 -4
  33. package/board/public/log.html +48 -0
  34. package/board/public/prompts.js +9 -8
  35. package/board/public/prototype-kit.js +6 -6
  36. package/board/public/prototype-switcher.js +13 -5
  37. package/board/public/prototype.html +1 -0
  38. package/board/public/router.js +8 -7
  39. package/board/public/rules-editor.js +3 -4
  40. package/board/public/shell.js +20 -6
  41. package/board/public/sidebar.css +21 -1
  42. package/board/public/sidebar.js +64 -31
  43. package/board/public/style.css +155 -62
  44. package/board/public/toasts.js +2 -2
  45. package/board/public/ui-cards.js +176 -71
  46. package/board/public/ui-enums.js +5 -4
  47. package/board/public/ui-header.js +10 -10
  48. package/board/public/ui-hub.js +4 -4
  49. package/board/public/ui-markdown.js +1 -1
  50. package/board/public/ui-order.js +9 -6
  51. package/board/public/ui-page-header.js +10 -7
  52. package/board/public/ui-patch.js +53 -0
  53. package/board/public/ui-phase-bars.js +17 -0
  54. package/board/public/ui-prompts.js +8 -8
  55. package/board/public/ui-questions.js +26 -23
  56. package/board/public/ui-sidebar.js +46 -22
  57. package/board/public/ui-snapshot.js +20 -9
  58. package/board/public/ui-threads.js +17 -11
  59. package/board/public/ui.js +80 -12
  60. package/board/public/view-frame.js +1 -1
  61. package/board/public/view-hub-settings.js +25 -18
  62. package/board/public/view-hub.js +53 -12
  63. package/board/public/view-notes.js +1 -1
  64. package/board/public/view-questions.js +101 -50
  65. package/board/public/view-settings.js +167 -49
  66. package/board/review.mjs +1 -1
  67. package/board/server.mjs +18 -12
  68. package/board/store.mjs +13 -12
  69. package/board/tickets.mjs +3 -3
  70. package/board/worktree.mjs +20 -14
  71. package/docs/hub-session.md +4 -4
  72. package/hooks-plugin/hooks/codex-share-auto.mjs +1 -1
  73. package/hooks-plugin/hooks/watcher-stop.mjs +2 -2
  74. package/hub/board-repo.mjs +1 -1
  75. package/hub/boards.mjs +4 -4
  76. package/hub/close-board.mjs +1 -1
  77. package/hub/github.mjs +4 -4
  78. package/hub/home.mjs +1 -1
  79. package/hub/hub.mjs +12 -12
  80. package/hub/incoming.mjs +2 -0
  81. package/hub/linear.mjs +2 -2
  82. package/hub/live.mjs +2 -2
  83. package/hub/new-board.mjs +2 -2
  84. package/hub/permission-denied-hook.mjs +2 -0
  85. package/hub/pr-report.mjs +3 -1
  86. package/hub/presence.mjs +1 -1
  87. package/hub/prompts.mjs +1 -1
  88. package/hub/push.mjs +2 -2
  89. package/hub/readiness.mjs +2 -2
  90. package/hub/repos.mjs +3 -3
  91. package/hub/restart-board.mjs +1 -1
  92. package/hub/schedule.mjs +1 -1
  93. package/hub/sessions.mjs +2 -2
  94. package/hub/sw.js +1 -1
  95. package/hub/try.mjs +8 -4
  96. package/hub/usage.mjs +3 -3
  97. package/hub/views.mjs +1 -1
  98. package/package.json +5 -1
  99. package/shared/card-prs.mjs +1 -0
  100. package/shared/colors.css +5 -4
  101. package/shared/fake-gh.mjs +59 -8
  102. package/shared/repos.mjs +1 -1
  103. package/shared/usage-gate.mjs +1 -1
  104. package/board/public/view-polling.js +0 -87
package/package.json CHANGED
@@ -1,8 +1,12 @@
1
1
  {
2
2
  "name": "@lovek4d/kickflip",
3
- "version": "0.3.1",
3
+ "version": "0.4.0",
4
4
  "description": "Follow and steer Claude Code sessions from your phone.",
5
5
  "license": "SEE LICENSE IN LICENSE",
6
+ "repository": {
7
+ "type": "git",
8
+ "url": "git+https://github.com/lovek4d/kickflip.git"
9
+ },
6
10
  "type": "module",
7
11
  "bin": {
8
12
  "kickflip": "bin/kickflip.mjs"
@@ -0,0 +1 @@
1
+ var s=Object.defineProperty;var t=(r,e)=>s(r,"name",{value:e,configurable:!0});const a=/^(?:([\w.-]+\/[\w.-]+)#)?([1-9]\d*)$/,o=t(r=>Array.isArray(r?.prs)?r.prs:Number.isInteger(r?.pr)?[{number:r.pr}]:[],"storedPRs"),l=t((r,e)=>o(r).map(n=>({repo:n.repo||e,number:n.number})).filter(n=>n.repo),"cardPRs"),m=t(r=>o(r).length>0,"hasPR"),p=t(({repo:r,number:e})=>`${r}#${e}`,"prKeyOf"),x=t(({repo:r,number:e})=>`https://github.com/${r}/pull/${e}`,"prURL"),d=t((r,e)=>r.map(n=>n.repo&&n.repo!==e?p(n):`#${n.number}`).join(", "),"prNames");function R(r){const e=a.exec(String(r).trim());return e?{...e[1]?{repo:e[1]}:{},number:Number(e[2])}:null}t(R,"parsePRRef");const i=["MERGED","CLOSED"],u=t(r=>r.checks?.state==="fail"||!!r.failing?.length,"failing");function c(r){return r?.state?i.includes(r.state)?5:r.isDraft?1:u(r)?2:r.reviewDecision==="APPROVED"?4:3:0}t(c,"readiness");function h(r){if(!r.length)return-1;const e=r.map(c);return e.every(n=>n===5)?Math.max(0,r.findIndex(n=>n.state==="MERGED")):e.indexOf(Math.min(...e))}t(h,"leastReady");const E=t(r=>r.length>0&&r.every(e=>i.includes(e?.state))&&r.some(e=>e.state==="MERGED"),"allLanded");export{E as allLanded,l as cardPRs,m as hasPR,h as leastReady,R as parsePRRef,p as prKeyOf,d as prNames,x as prURL,o as storedPRs};
package/shared/colors.css CHANGED
@@ -1,15 +1,16 @@
1
- /* The one colour set for the hub and every board (docs/adr/0033-one-colour-constants-file-shared-by-the-hub-and-every-board.md).
1
+ /* The one colour set for the hub and every board (docs/adr/2026-10-29-1639-one-colour-constants-file-shared-by-the-hub-and-every-board.md).
2
2
  board/server.mjs serves it as colors.css and hub/hub.mjs as /_hub/colors.css, linked before each page's own CSS.
3
3
  --accent is the one human blue: new messages, links, buttons, focus rings, the owner's bubble (--accent-soft) and
4
- planner lines; --on-accent is the label on an accent fill (docs/adr/0169-one-human-blue.md). */
4
+ planner lines; --on-accent is the label on an accent fill (docs/adr/2026-11-03-1627-one-human-blue.md). */
5
5
  :root {
6
6
  --bg: #fbfbfc; --panel: #ffffff; --line: #e6e6ea; --text: #1b1b1f; --muted: #6b6b76;
7
- --accent: #2b6cb0; --accent-soft: #eef6ff; --on-accent: #ffffff; --ok: #1f9d55; --warn: #c27c0e; --bad: #d1435b;
7
+ --accent: #2b6cb0; --accent-soft: #eef6ff; --on-accent: #ffffff; --ok: #1f9d55; --warn: #c27c0e; --bad: #d1435b; --purple: #8250df;
8
8
  --chip: #f1f1f4; --band: #f2f2f5; --claude: #fdf3e4;
9
9
  --color-quiet: var(--muted); --color-info: var(--accent); --color-notice: var(--warn); --color-danger: var(--bad); --color-success: var(--ok);
10
+ --color-merged: var(--purple);
10
11
  }
11
12
  @media (prefers-color-scheme: dark) {
12
13
  :root { --bg: #111114; --panel: #18181c; --line: #2a2a31; --text: #ececf1; --muted: #9a9aa6;
13
- --accent: #6fa8e8; --accent-soft: #1b2533; --on-accent: #0b0b0e; --ok: #3ccf7f; --warn: #e3a33a; --bad: #f0687f;
14
+ --accent: #6fa8e8; --accent-soft: #1b2533; --on-accent: #0b0b0e; --ok: #3ccf7f; --warn: #e3a33a; --bad: #f0687f; --purple: #ab7df8;
14
15
  --chip: #24242b; --band: #202026; --claude: #2d2416; }
15
16
  }
@@ -1,5 +1,5 @@
1
- var g=Object.defineProperty;var r=(e,s)=>g(e,"name",{value:s,configurable:!0});import n from"node:fs";import a from"node:path";import{PR_VIEW_FIELDS as p}from"../hub/pr-report.mjs";const i=r((e,s,t,c)=>{const o=a.join(e,s,`${t}.json`);n.mkdirSync(a.dirname(o),{recursive:!0}),n.writeFileSync(o,JSON.stringify(c))},"write"),S=r((e,s,t)=>i(e,"pr",s,t),"writePR"),j=r((e,s,t)=>i(e,"issue-comments",s,t),"writeIssueComments"),v=r((e,s,t)=>i(e,"review-comments",s,t),"writeReviewComments"),y=r((e,s,t)=>i(e,"reviews",s,t),"writeReviews"),w=r((e,s,t)=>i(e,"merge",s,t),"writeMerge"),x=r((e,s)=>i(e,"","user",s),"writeUser"),O=r((e,s)=>i(e,"","accounts",s),"writeAccounts"),l=r((e,s)=>{try{return n.readFileSync(a.join(e,s),"utf8").trim().split(`
2
- `).filter(Boolean).map(JSON.parse)}catch{return[]}},"jsonl"),E=r(e=>l(e,"calls.jsonl"),"calls"),N=r(e=>l(e,"tokens.jsonl"),"tokens");function R({head:e="abc1234567",failing:s=[],pending:t=0,...c}={}){return{state:"OPEN",isDraft:!1,reviewDecision:"REVIEW_REQUIRED",mergeable:"MERGEABLE",mergeStateStatus:"BLOCKED",headRefOid:e,statusCheckRollup:[...s.map(o=>({__typename:"CheckRun",name:o,status:"COMPLETED",conclusion:"FAILURE"})),...Array.from({length:t},(o,u)=>({__typename:"CheckRun",name:`ci-${u}`,status:"IN_PROGRESS",conclusion:""}))],...c}}r(R,"prView");function k(e){const s=a.join(e,"bin");return n.mkdirSync(s,{recursive:!0}),n.writeFileSync(a.join(s,"gh"),`#!${process.execPath}
1
+ var p=Object.defineProperty;var t=(e,r)=>p(e,"name",{value:r,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((e,r,s,i)=>{const a=l.join(e,r,`${String(s).replace(/[/#]/g,"__")}.json`);n.mkdirSync(l.dirname(a),{recursive:!0}),n.writeFileSync(a,JSON.stringify(i))},"write"),w=t((e,r,s)=>o(e,"pr",r,s),"writePR"),x=t((e,r,s)=>o(e,"issue-comments",r,s),"writeIssueComments"),v=t((e,r,s)=>o(e,"review-comments",r,s),"writeReviewComments"),b=t((e,r,s)=>o(e,"reviews",r,s),"writeReviews"),S=t((e,r,s)=>o(e,"merge",r,s),"writeMerge"),_=t((e,r,s)=>o(e,"ready",r,s),"writeReady"),j=t((e,r)=>o(e,"","user",r),"writeUser"),R=t((e,r)=>o(e,"","accounts",r),"writeAccounts"),c=t((e,r)=>{try{return n.readFileSync(l.join(e,r),"utf8").trim().split(`
2
+ `).filter(Boolean).map(JSON.parse)}catch{return[]}},"jsonl"),f=t(e=>c(e,"calls.jsonl"),"calls"),O=t((e,r)=>o(e,"","rate",r),"writeRate"),N=t(e=>f(e).filter(r=>r[0]==="api"&&r[1]==="graphql").map(r=>[...r[3].matchAll(/repository\(owner: "([^"]+)", name: "([^"]+)"\) \{ pullRequest\(number: (\d+)\)/g)].map(([,s,i,a])=>`${s}/${i}#${a}`)),"queried"),k=t(e=>c(e,"tokens.jsonl"),"tokens");function E({head:e="abc1234567",failing:r=[],pending:s=0,...i}={}){return{state:"OPEN",isDraft:!1,reviewDecision:"REVIEW_REQUIRED",mergeable:"MERGEABLE",mergeStateStatus:"BLOCKED",headRefOid:e,statusCheckRollup:[...r.map(a=>({__typename:"CheckRun",name:a,status:"COMPLETED",conclusion:"FAILURE"})),...Array.from({length:s},(a,u)=>({__typename:"CheckRun",name:`ci-${u}`,status:"IN_PROGRESS",conclusion:""}))],...i}}t(E,"prView");function J(e){const r=l.join(e,"bin");return n.mkdirSync(r,{recursive:!0}),n.writeFileSync(l.join(r,"gh"),`#!${process.execPath}
3
3
  const fs = require('node:fs');
4
4
  const path = require('node:path');
5
5
  const dir = ${JSON.stringify(e)};
@@ -7,6 +7,11 @@ 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
+ // The repo's own fixture for PR n, else the one for n in any repo.
11
+ const fixture = (folder, repo, n) => {
12
+ const own = path.join(dir, folder, repo.replace('/', '__') + '__' + n + '.json');
13
+ return fs.existsSync(own) ? own : path.join(dir, folder, n + '.json');
14
+ };
10
15
  if (args.length === 4 && args[0] === 'auth' && args[1] === 'token' && args[2] === '-u') {
11
16
  let accounts = {};
12
17
  try { accounts = JSON.parse(fs.readFileSync(path.join(dir, 'accounts.json'), 'utf8')); } catch {}
@@ -25,18 +30,64 @@ if (args[0] === 'issue' && args.includes('--repo')) {
25
30
  }
26
31
  if (args.length === 6 && args[0] === 'pr' && args[1] === 'merge' && /^\\d+$/.test(args[2]) && args[3] === '--repo' && args[5] === '--squash') {
27
32
  let refused = null;
28
- try { refused = JSON.parse(fs.readFileSync(path.join(dir, 'merge', args[2] + '.json'), 'utf8')).error; } catch {}
33
+ try { refused = JSON.parse(fs.readFileSync(fixture('merge', args[4], args[2]), 'utf8')).error; } catch {}
29
34
  if (refused) { console.error(refused); process.exit(1); }
30
35
  process.exit(0);
31
36
  }
37
+ if (args.length === 5 && args[0] === 'pr' && args[1] === 'ready' && /^\\d+$/.test(args[2]) && args[3] === '--repo') {
38
+ let refused = null;
39
+ try { refused = JSON.parse(fs.readFileSync(fixture('ready', args[4], args[2]), 'utf8')).error; } catch {}
40
+ if (refused) { console.error(refused); process.exit(1); }
41
+ console.log('\u2713 Pull request ' + args[4] + '#' + args[2] + ' is marked as "ready for review"');
42
+ process.exit(0);
43
+ }
44
+ // A comment or review-comment reply posted with gh api <endpoint> -f body=\u2026: answers the endpoint as its html_url.
45
+ if (args.length === 4 && args[0] === 'api' && args[2] === '-f' && args[3].startsWith('body=') && /^repos\\/[^/]+\\/[^/]+\\/(issues\\/\\d+\\/comments|pulls\\/\\d+\\/comments\\/\\d+\\/replies)$/.test(args[1])) {
46
+ console.log(JSON.stringify({ html_url: 'https://github.com/' + args[1].slice('repos/'.length) }));
47
+ process.exit(0);
48
+ }
49
+ // The hub's one GraphQL query (hub/pr-report.mjs fetchPRs), answered from the same fixtures as the REST calls: each
50
+ // aliased PR from its pr, issue-comments, review-comments and reviews fixtures, the viewer from user.json and the rate
51
+ // limit from rate.json. A PR without a fixture, or {fail: true}, comes back null with an error, as GitHub answers.
52
+ if (args.length === 4 && args[0] === 'api' && args[1] === 'graphql' && args[2] === '-f' && args[3].startsWith('query=')) {
53
+ const read = (file, fallback) => { try { return JSON.parse(fs.readFileSync(file, 'utf8')); } catch { return fallback; } };
54
+ const rate = read(path.join(dir, 'rate.json'), { remaining: 4990, resetAt: '2026-10-23T12:00:00Z' });
55
+ if (rate.fail) fail();
56
+ const user = read(path.join(dir, 'user.json'), null);
57
+ const author = (u) => (u?.login ? (/\\[bot\\]$/.test(u.login) ? { __typename: 'Bot', login: u.login.replace(/\\[bot\\]$/, '') } : { __typename: 'User', login: u.login }) : null);
58
+ const list = (folder, repo, n) => { const v = read(fixture(folder, repo, n), []); return Array.isArray(v) ? v : []; };
59
+ const data = { viewer: user ? { login: user.login } : null, rateLimit: rate };
60
+ const errors = [];
61
+ for (const [, alias, owner, name, n] of args[3].matchAll(/(\\w+): repository\\(owner: "([^"]+)", name: "([^"]+)"\\) \\{ pullRequest\\(number: (\\d+)\\)/g)) {
62
+ const repo = owner + '/' + name;
63
+ const view = read(fixture('pr', repo, n), null);
64
+ if (!view || view.fail) { data[alias] = { pullRequest: null }; errors.push({ path: [alias, 'pullRequest'], message: 'Could not resolve to a PullRequest with the number of ' + n + '.' }); continue; }
65
+ const { statusCheckRollup = [], ...fields } = view;
66
+ data[alias] = { pullRequest: { ...fields,
67
+ commits: { nodes: [{ commit: { statusCheckRollup: { contexts: { nodes: statusCheckRollup } } } }] },
68
+ comments: { nodes: list('issue-comments', repo, n).map((c) => ({ databaseId: c.id, author: author(c.user), createdAt: c.created_at, body: c.body, url: c.html_url })) },
69
+ reviews: { nodes: list('reviews', repo, n).map((r) => ({ databaseId: r.id, author: author(r.user), submittedAt: r.submitted_at, body: r.body, state: r.state, url: r.html_url })) },
70
+ reviewThreads: { nodes: list('review-comments', repo, n).map((c) => ({ comments: { nodes: [{ databaseId: c.id, author: author(c.user), createdAt: c.created_at, body: c.body, url: c.html_url, path: c.path, line: c.line, replyTo: c.in_reply_to_id == null ? null : { databaseId: c.in_reply_to_id } }] } })) },
71
+ } };
72
+ }
73
+ console.log(JSON.stringify(errors.length ? { data, errors } : { data }));
74
+ if (errors.length) { console.error('gh: ' + errors[0].message); process.exit(1); }
75
+ process.exit(0);
76
+ }
77
+ // gh pr create --repo <repo> \u2026: answers the new PR's URL, numbered 100 plus the creates before it.
78
+ if (args[0] === 'pr' && args[1] === 'create' && args.includes('--repo')) {
79
+ const n = fs.readFileSync(path.join(dir, 'calls.jsonl'), 'utf8').split('\\n').filter((l) => l.startsWith('["pr","create"')).length;
80
+ console.log('https://github.com/' + args[args.indexOf('--repo') + 1] + '/pull/' + (99 + n));
81
+ process.exit(0);
82
+ }
32
83
  let file;
33
84
  let list = false;
34
- if (args.length === 7 && args[0] === 'pr' && args[1] === 'view' && /^\\d+$/.test(args[2]) && args[3] === '--repo' && args[5] === '--json' && args[6] === ${JSON.stringify(p)}) file = path.join(dir, 'pr', args[2] + '.json');
85
+ if (args.length === 7 && args[0] === 'pr' && args[1] === 'view' && /^\\d+$/.test(args[2]) && args[3] === '--repo' && args[5] === '--json' && args[6] === ${JSON.stringify(g)}) file = fixture('pr', args[4], args[2]);
35
86
  else if (args.length === 2 && args[0] === 'api' && args[1] === 'user') file = path.join(dir, 'user.json');
36
87
  else if (args.length === 4 && args[0] === 'api' && args[1] === '--paginate' && args[2] === '--slurp') {
37
- const match = /^repos\\/[^/]+\\/[^/]+\\/(issues|pulls)\\/(\\d+)\\/(comments\\?since=.+|reviews)$/.exec(args[3]);
38
- if (!match || (match[1] === 'issues' && match[3] === 'reviews')) process.exit(1);
39
- file = path.join(dir, match[3] === 'reviews' ? 'reviews' : match[1] === 'issues' ? 'issue-comments' : 'review-comments', match[2] + '.json');
88
+ const match = /^repos\\/([^/]+\\/[^/]+)\\/(issues|pulls)\\/(\\d+)\\/(comments\\?since=.+|reviews)$/.exec(args[3]);
89
+ if (!match || (match[2] === 'issues' && match[4] === 'reviews')) process.exit(1);
90
+ file = fixture(match[4] === 'reviews' ? 'reviews' : match[2] === 'issues' ? 'issue-comments' : 'review-comments', match[1], match[3]);
40
91
  list = true;
41
92
  } else process.exit(1);
42
93
  let value;
@@ -51,4 +102,4 @@ if (list) {
51
102
  try { if (JSON.parse(value).fail) fail(); } catch {}
52
103
  console.log(value);
53
104
  }
54
- `,{mode:493}),s}r(k,"fakeGh");export{E as calls,k as fakeGh,R as prView,N as tokens,O as writeAccounts,j as writeIssueComments,w as writeMerge,S as writePR,v as writeReviewComments,y as writeReviews,x as writeUser};
105
+ `,{mode:493}),r}t(J,"fakeGh");export{f as calls,J as fakeGh,E as prView,N as queried,k as tokens,R as writeAccounts,x as writeIssueComments,S as writeMerge,w as writePR,O as writeRate,_ as writeReady,v as writeReviewComments,b as writeReviews,j as writeUser};
package/shared/repos.mjs CHANGED
@@ -1 +1 @@
1
- var t=Object.defineProperty;var e=(r,i)=>t(r,"name",{value:i,configurable:!0});import m from"node:fs";import p from"node:os";import o from"node:path";import{fileURLToPath as s}from"node:url";const f=e(()=>o.resolve(o.dirname(s(import.meta.url)),".."),"cloneRoot"),h=e((r=f())=>m.existsSync(o.join(r,".git"))?o.resolve(r,".repos"):o.join(p.homedir(),".claude","kickflip","repos"),"reposDir");export{f as cloneRoot,h as reposDir};
1
+ var i=Object.defineProperty;var r=(o,t)=>i(o,"name",{value:t,configurable:!0});import m from"node:os";import e from"node:path";import{fileURLToPath as p}from"node:url";const f=r(()=>e.resolve(e.dirname(p(import.meta.url)),".."),"cloneRoot"),n=r((o=m.homedir())=>e.join(o,".claude","kickflip","repos"),"reposDir");export{f as cloneRoot,n as reposDir};
@@ -1 +1 @@
1
- var S=Object.defineProperty;var o=(e,t)=>S(e,"name",{value:t,configurable:!0});import g from"node:os";import{usageReader as $}from"../hub/usage.mjs";import{readSettingsFile as x,writeSettingsFile as m,readCodexShare as A,readClaudeModel as D}from"../hub/push.mjs";import{codexStatus as N,codexStatusReader as M,effectiveCodexShare as O}from"./codex.mjs";const E=o(e=>Number.isInteger(e)&&e>=50&&e<=100&&e%5===0,"validPauseAt"),h=o(e=>Number(e),"timeOf"),G=["five_hour","seven_day"];function P({claude:e,codex:t,pauseAt:s=90,keepGoing:n,now:a=Date.now()}={}){const i=E(s)?s:90,r=o(f=>i===100?null:G.find(d=>{const c=f?.[d];return c?.state==="live"&&Number.isFinite(c.used_percentage)&&c.used_percentage>=i&&(!c.resets_at||Date.parse(c.resets_at)>h(a))})||null,"past"),l=o((f,d)=>({window:d,used:d?f[d].used_percentage:null,resets_at:d?f[d].resets_at:null}),"details"),u=r(e),p=l(e,u),C=u&&n?.window===u&&Math.abs(Date.parse(n.until)-Date.parse(p.resets_at))<=3e5&&h(a)<Date.parse(n.until),w=r(t);return{pause_at:i,state:u?C?"going":"paused":"normal",...p,codex:{past:!!w,...l(t,w)}}}o(P,"usageGate");const _=new Map;function k({home:e=g.homedir(),now:t=Date.now()}={}){let s=_.get(e);s||(s={now:h(t)},s.usage=$({home:e,now:o(()=>s.now,"now")}),s.codex=M({home:e,now:o(()=>s.now,"now")}),_.set(e,s)),s.now=h(t);const n=x(e);return P({...s.usage({codexShare:A(s.codex(),e)}),pauseAt:n.pauseAt,keepGoing:n.keepGoing,now:t})}o(k,"readUsageGate");function U(e,{home:t=g.homedir()}={}){if(!/^(?:50|55|60|65|70|75|80|85|90|95|100)$/.test(String(e)))throw Object.assign(new Error("pause-at takes 50..100 in steps of 5"),{status:400});m({...x(t),pauseAt:Number(e)},t)}o(U,"setPauseAt");function W(e,{home:t=g.homedir(),now:s=Date.now()}={}){if(!["on","off"].includes(e))throw Object.assign(new Error("keep-going takes on or off"),{status:400});const n=x(t);if(e==="on"){const a=k({home:t,now:s});if(a.state!=="paused")throw Object.assign(new Error("Nothing is paused."),{status:409});n.keepGoing={window:a.window,until:a.resets_at}}else delete n.keepGoing;m(n,t)}o(W,"setKeepGoing");const z=o(e=>{try{return["github-change","check-github","card-unblocked","card-pull","card-plan","todo-triage"].includes(JSON.parse(e).kind)}catch{return!1}},"heldUsageEvent");function B({home:e=g.homedir(),now:t=Date.now(),card:s={}}={}){const n=o(l=>{const{model:u,reason:p}=R(D(e),s);return{kind:"claude",model:u,reason:`${l} ${p}`}},"claude"),a=N({home:e});if(!a.usable)return n(a.words);const i=O(x(e).codexShare,a);if(i==="off")return n("Codex share is off.");const r=k({home:e,now:t});return r.codex.past?n(`Codex is at ${Math.round(r.codex.used)}%, past the ${r.pause_at}% line until ${r.codex.resets_at}.`):{kind:"codex",reason:`Codex share is ${i} and Codex is under the ${r.pause_at}% line.`}}o(B,"builderPick");const b={opus:"Opus",sonnet:"Sonnet"};function R(e,t){return e!=="auto"?{model:e,reason:`Claude model is ${b[e]}.`}:t.model?{model:t.model,reason:`Claude model is Auto and the card asks for Claude ${b[t.model]}.`}:t.size==="small"?{model:"sonnet",reason:"Claude model is Auto and the card is small."}:{model:"opus",reason:"Claude model is Auto and the card isn't small."}}o(R,"claudePick");const H=o(e=>`Work is paused: Claude's ${e.window==="five_hour"?"5-hour":"weekly"} usage is at ${Math.round(e.used)}%. Resumes at ${e.resets_at}. The owner can tap Keep going on the Hub page.`,"pauseReason");export{B as builderPick,z as heldUsageEvent,H as pauseReason,k as readUsageGate,W as setKeepGoing,U as setPauseAt,P as usageGate,E as validPauseAt};
1
+ var S=Object.defineProperty;var o=(e,t)=>S(e,"name",{value:t,configurable:!0});import g from"node:os";import{usageReader as $}from"../hub/usage.mjs";import{readSettingsFile as x,writeSettingsFile as m,readCodexShare as A,readClaudeModel as D}from"../hub/push.mjs";import{codexStatus as N,codexStatusReader as M,effectiveCodexShare as O}from"./codex.mjs";const E=o(e=>Number.isInteger(e)&&e>=50&&e<=100&&e%5===0,"validPauseAt"),h=o(e=>Number(e),"timeOf"),G=["five_hour","seven_day"];function v({claude:e,codex:t,pauseAt:s=90,keepGoing:n,now:a=Date.now()}={}){const d=E(s)?s:90,r=o(f=>d===100?null:G.find(i=>{const c=f?.[i];return c?.state==="live"&&Number.isFinite(c.used_percentage)&&c.used_percentage>=d&&(!c.resets_at||Date.parse(c.resets_at)>h(a))})||null,"past"),l=o((f,i)=>({window:i,used:i?f[i].used_percentage:null,resets_at:i?f[i].resets_at:null}),"details"),u=r(e),p=l(e,u),C=u&&n?.window===u&&Math.abs(Date.parse(n.until)-Date.parse(p.resets_at))<=3e5&&h(a)<Date.parse(n.until),w=r(t);return{pause_at:d,state:u?C?"going":"paused":"normal",...p,codex:{past:!!w,...l(t,w)}}}o(v,"usageGate");const _=new Map;function k({home:e=g.homedir(),now:t=Date.now()}={}){let s=_.get(e);s||(s={now:h(t)},s.usage=$({home:e,now:o(()=>s.now,"now")}),s.codex=M({home:e,now:o(()=>s.now,"now")}),_.set(e,s)),s.now=h(t);const n=x(e);return v({...s.usage({codexShare:A(s.codex(),e)}),pauseAt:n.pauseAt,keepGoing:n.keepGoing,now:t})}o(k,"readUsageGate");function U(e,{home:t=g.homedir()}={}){if(!/^(?:50|55|60|65|70|75|80|85|90|95|100)$/.test(String(e)))throw Object.assign(new Error("pause-at takes 50..100 in steps of 5"),{status:400});m({...x(t),pauseAt:Number(e)},t)}o(U,"setPauseAt");function W(e,{home:t=g.homedir(),now:s=Date.now()}={}){if(!["on","off"].includes(e))throw Object.assign(new Error("keep-going takes on or off"),{status:400});const n=x(t);if(e==="on"){const a=k({home:t,now:s});if(a.state!=="paused")throw Object.assign(new Error("Nothing is paused."),{status:409});n.keepGoing={window:a.window,until:a.resets_at}}else delete n.keepGoing;m(n,t)}o(W,"setKeepGoing");const z=o(e=>{try{return["github-change","check-github","card-unblocked","card-pull","card-plan","overnight-triage","pr-review"].includes(JSON.parse(e).kind)}catch{return!1}},"heldUsageEvent");function B({home:e=g.homedir(),now:t=Date.now(),card:s={}}={}){const n=o(l=>{const{model:u,reason:p}=P(D(e),s);return{kind:"claude",model:u,reason:`${l} ${p}`}},"claude"),a=N({home:e});if(!a.usable)return n(a.words);const d=O(x(e).codexShare,a);if(d==="off")return n("Codex share is off.");const r=k({home:e,now:t});return r.codex.past?n(`Codex is at ${Math.round(r.codex.used)}%, past the ${r.pause_at}% line until ${r.codex.resets_at}.`):{kind:"codex",reason:`Codex share is ${d} and Codex is under the ${r.pause_at}% line.`}}o(B,"builderPick");const b={opus:"Opus",sonnet:"Sonnet"};function P(e,t){return e!=="auto"?{model:e,reason:`Claude model is ${b[e]}.`}:t.model?{model:t.model,reason:`Claude model is Auto and the card asks for Claude ${b[t.model]}.`}:t.size==="small"?{model:"sonnet",reason:"Claude model is Auto and the card is small."}:{model:"opus",reason:"Claude model is Auto and the card isn't small."}}o(P,"claudePick");const H=o(e=>`Work is paused: Claude's ${e.window==="five_hour"?"5-hour":"weekly"} usage is at ${Math.round(e.used)}%. Resumes at ${e.resets_at}. The owner can tap Keep going on the Hub page.`,"pauseReason");export{B as builderPick,z as heldUsageEvent,H as pauseReason,k as readUsageGate,W as setKeepGoing,U as setPauseAt,v as usageGate,E as validPauseAt};
@@ -1,87 +0,0 @@
1
- // HubPolling, the editor inside Hub’s Polling modal (ADR 0075): the day’s windows, overnight hold
2
- // and daily wakes, in hub/views.mjs pollingView's words from GET /_hub/api/polling. ?w= names the window shown,
3
- // which a tap on the day bar changes in place. Each change posts to /_hub/settings/polling and saves at once.
4
- (function () {
5
- // The last data, which a return to the view paints at once.
6
- let last = null;
7
- const { esc } = HubUI;
8
- const ACTION = '/_hub/settings/polling';
9
- // Each change posts op and w, the shown window's start.
10
- const form = (op, w, inner, attrs = '') => `<form method="post" action="${ACTION}"${attrs}>${op ? `<input type="hidden" name="op" value="${op}">` : ''}<input type="hidden" name="w" value="${esc(w)}">${inner}</form>`;
11
-
12
- function dayBar(day, sel) {
13
- const segs = day.segments.map((g) => `<a class="seg${g.w === sel ? ' sel' : ''}${g.held ? ' held' : ''}" href="${esc(AppRouter.urlOf({ kind: 'hub', hash: '#polling', w: g.w }))}" data-w="${esc(g.w)}" style="left:${g.left};width:${g.width};--k:${g.shade}"`
14
- + ` aria-label="${esc(g.aria)}"${g.w === sel ? ' aria-current="true"' : ''}>${g.label ? `<span>${esc(g.label)}</span>` : ''}</a>`).join('');
15
- const pins = day.pins.map((p) => `<span class="pin" style="left:${p.left}" title="${esc(p.title)}" aria-hidden="true">▼</span>`).join('');
16
- const ticks = ['12am', '6am', '12pm', '6pm', '12am'].map((t, i) => `<span style="left:${i * 25}%">${t}</span>`).join('');
17
- const legend = ['<span><i style="background:var(--muted)"></i>darker checks more often</span>',
18
- day.held && '<span><i style="background:repeating-linear-gradient(135deg,var(--muted) 0 3px,var(--chip) 3px 5px)"></i>pushes held</span>',
19
- day.wakes && '<span style="color:var(--color-notice)">▼ wake</span>',
20
- '<span><i style="background:var(--text);width:2px"></i>now</span>'].filter(Boolean).join('');
21
- return `<div class="day">${pins}<span class="now" style="left:${day.now}" aria-hidden="true"></span><div class="bar">${segs}</div><div class="ticks" aria-hidden="true">${ticks}</div></div><div class="legend">${legend}</div>`;
22
- }
23
-
24
- function html(d, sel, flash) {
25
- const { row, tile, nowRow } = HubUI;
26
- const w = d.windows.find((x) => x.from === sel);
27
- const chips = w.every.map((c) => `<button class="btn${c.on ? '' : ' quiet'}" name="value" value="${c.minutes}"${c.on ? ' aria-pressed="true"' : ''}>${esc(c.label)}</button>`).join('');
28
- const wakeChips = d.wakes.map((t) => `<button class="btn quiet" name="value" value="${esc(t.value)}" aria-label="Remove the ${esc(t.label)} wake">${esc(t.label)} ✕</button>`).join('');
29
- return `${HubUI.flash(flash)}<h2 class="label" style="margin-top:4px">Now</h2>
30
- <div class="group">${nowRow(d.now)}</div>
31
- <h2 class="label" id="day">Your day</h2>
32
- <div class="group"><div class="block">${dayBar(d.day, w.from)}</div></div>
33
- <h2 class="label" id="window">${esc(w.range)}</h2>
34
- <div class="group">
35
- ${form('from', w.from, `<label class="body"><span class="text title">Starts</span><input type="time" name="value" value="${esc(w.from)}" required data-autosave></label>`, ' class="row field"')}
36
- ${row({ title: 'Ends', trail: `<span class="val">${esc(w.ends)}</span>` })}
37
- ${form('every', w.from, `<span class="title">Checks every</span><span class="chips">${chips}</span><span class="sub" style="margin-top:6px">${esc(w.checks)}</span>`, ' class="block"')}
38
- ${form('', w.from, `<button class="btn quiet" name="op" value="split"${w.split ? '' : ' disabled'}>Split in two</button><button class="btn quiet danger" name="op" value="remove"${w.remove ? '' : ' disabled'}>Remove</button>`, ' class="block split"')}
39
- </div>
40
- <h2 class="label" id="quiet">Quiet times</h2>
41
- <div class="group">${form('hold', w.from, `${tile('moon', '#6366f1')}<label class="body"><span class="text"><span class="title">Hold pushes overnight</span></span><input type="checkbox" name="value" value="on" class="toggle" data-submit${d.hold.on ? ' checked' : ''}></label>`, ' class="row field"')}</div>
42
- <h2 class="label" id="wakes">Daily wakes</h2>
43
- <div class="group"><div class="block"><div class="chips" style="margin-top:0">${wakeChips ? form('wake-remove', w.from, wakeChips) : ''}${form('wake-add', w.from, '<input type="time" name="value" required aria-label="New wake time"><button class="btn quiet">+ Add</button>', ' class="field"')}</div></div></div>`;
44
- }
45
-
46
- globalThis.HubPolling = {
47
- create: (root, siteRoot, ctx) => BoardView.page(root, siteRoot, (B) => {
48
- const main = root;
49
- let sel = ctx.view?.w || null;
50
- let flash = null;
51
- const shown = () => (last.windows.some((x) => x.from === sel) ? sel : last.select);
52
- const paint = () => { if (last) main.innerHTML = html(last, shown(), flash); };
53
- // The shown window lives in the URL, so a reload or a shared link shows it too.
54
- const select = (w) => {
55
- sel = w;
56
- history.replaceState(history.state, '', AppRouter.urlOf({ kind: 'hub', hash: '#polling', w }));
57
- paint();
58
- };
59
- B.selectWindow = select;
60
- B.on(root, 'click', (e) => {
61
- const seg = e.target.closest?.('a.seg[data-w]');
62
- if (!seg || e.metaKey || e.ctrlKey || e.shiftKey || e.altKey) return;
63
- e.preventDefault();
64
- select(seg.dataset.w);
65
- });
66
- const forms = HubKit.forms(B, {
67
- answer(out, action) {
68
- if (action === ACTION) { last = out; flash = null; select(out.select); }
69
- else { flash = { text: out.message, error: !out.ok }; load(); }
70
- ctx.changed?.();
71
- ctx.sidebarChanged?.();
72
- },
73
- fail(message) { flash = { text: message, error: true }; paint(); },
74
- });
75
- const load = HubKit.live(B, {
76
- url: () => `/_hub/api/polling${sel ? `?w=${encodeURIComponent(sel)}` : ''}`,
77
- paint(d) {
78
- if (!d) { if (!last) main.innerHTML = '<p class="note">Offline. Is the host awake?</p>'; return; }
79
- last = d;
80
- paint();
81
- },
82
- busy: () => forms.busy() || Boolean(root.querySelector('input:focus')),
83
- });
84
- paint();
85
- }),
86
- };
87
- })();