pi-lens 2.0.37 โ†’ 2.0.39

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 (46) hide show
  1. package/CHANGELOG.md +16 -0
  2. package/README.md +1 -1
  3. package/clients/architect-client.js +50 -20
  4. package/clients/architect-client.ts +61 -22
  5. package/clients/ast-grep-client.js +32 -127
  6. package/clients/ast-grep-client.test.js +2 -14
  7. package/clients/ast-grep-client.test.ts +2 -16
  8. package/clients/ast-grep-client.ts +34 -150
  9. package/clients/auto-loop.js +117 -0
  10. package/clients/auto-loop.ts +171 -0
  11. package/clients/biome-client.js +25 -22
  12. package/clients/biome-client.ts +28 -25
  13. package/clients/complexity-client.js +111 -74
  14. package/clients/complexity-client.ts +149 -105
  15. package/clients/dependency-checker.js +16 -30
  16. package/clients/dependency-checker.ts +15 -35
  17. package/clients/fix-scanners.js +195 -0
  18. package/clients/fix-scanners.ts +297 -0
  19. package/clients/interviewer-templates.js +75 -0
  20. package/clients/interviewer-templates.ts +90 -0
  21. package/clients/interviewer.js +73 -101
  22. package/clients/interviewer.ts +195 -140
  23. package/clients/knip-client.js +12 -4
  24. package/clients/knip-client.ts +21 -16
  25. package/clients/metrics-history.js +215 -0
  26. package/clients/metrics-history.ts +300 -0
  27. package/clients/scan-architectural-debt.js +62 -72
  28. package/clients/scan-architectural-debt.ts +79 -64
  29. package/clients/scan-utils.js +98 -0
  30. package/clients/scan-utils.ts +112 -0
  31. package/clients/sg-runner.js +138 -0
  32. package/clients/sg-runner.ts +168 -0
  33. package/clients/subprocess-client.js +0 -37
  34. package/clients/subprocess-client.ts +0 -60
  35. package/clients/ts-service.ts +1 -7
  36. package/clients/type-safety-client.js +3 -8
  37. package/clients/type-safety-client.ts +10 -14
  38. package/clients/typescript-client.js +55 -56
  39. package/clients/typescript-client.ts +94 -52
  40. package/default-architect.yaml +87 -0
  41. package/index.ts +143 -1165
  42. package/package.json +2 -1
  43. package/rules/ast-grep-rules/rules/large-class.yml +6 -2
  44. package/rules/ast-grep-rules/rules/long-method.yml +1 -1
  45. package/rules/ast-grep-rules/rules/no-single-char-var.yml +2 -2
  46. package/rules/ast-grep-rules/rules/switch-without-default.yml +0 -12
@@ -0,0 +1,75 @@
1
+ export const CONFIRMATION_HTML = (question, plan, diff, esc, mdToHtml, diffHtml, addCount, delCount) => `<!DOCTYPE html><html lang="en"><head><meta charset="UTF-8"><meta name="viewport" content="width=device-width,initial-scale=1">
2
+ <title>๐Ÿ—๏ธ Changes Applied</title>
3
+ <style>
4
+ *{box-sizing:border-box;margin:0;padding:0}body{font-family:-apple-system,BlinkMacSystemFont,'Segoe UI',sans-serif;background:#0d1117;color:#e6edf3;padding:28px 32px;max-width:960px;margin:0 auto;line-height:1.5}
5
+ h2{font-size:16px;color:#58a6ff;margin-bottom:14px}
6
+ .plan{background:#161b22;border:1px solid #30363d;border-radius:8px;padding:14px 18px;margin-bottom:18px;font-size:13px;line-height:1.6}
7
+ .plan h3{color:#f0f6fc;font-size:14px;margin:10px 0 4px}.plan h4{color:#c9d1d9;font-size:13px;margin:8px 0 3px}
8
+ .plan li{margin:2px 0 2px 16px;list-style:disc}.plan code{background:#21262d;padding:1px 5px;border-radius:3px;font-size:12px}
9
+ .diff-wrap{background:#161b22;border:1px solid #30363d;border-radius:8px;margin-bottom:18px;overflow:hidden}
10
+ .diff-hdr{padding:7px 14px;font-size:11px;color:#8b949e;border-bottom:1px solid #30363d;font-family:monospace;display:flex;justify-content:space-between}
11
+ .diff-stats{display:flex;gap:10px}.stat-add{color:#3fb950}.stat-del{color:#ff7b72}
12
+ .diff-pre{padding:12px;font-family:'Fira Code',Consolas,monospace;font-size:12px;line-height:1.55;overflow-x:auto;white-space:pre;margin:0}
13
+ .da{color:#3fb950;display:block}.dd{color:#ff7b72;display:block}.dh{color:#79c0ff;display:block}.df{color:#8b949e;display:block}.dc{color:#e6edf3;display:block}
14
+ .actions{display:flex;gap:10px;flex-wrap:wrap}
15
+ .btn-c{background:#238636;color:#fff;border:1px solid #2ea043;padding:10px 24px;border-radius:6px;font-size:14px;font-weight:600;cursor:pointer}.btn-c:hover{background:#2ea043}
16
+ .btn-chat{background:#1a2332;color:#79c0ff;border:1px solid #1f6feb;padding:10px 24px;border-radius:6px;font-size:14px;cursor:pointer}.btn-chat:hover{background:#1f3050}
17
+ .chat-area{display:none;margin-top:12px}
18
+ textarea{width:100%;background:#161b22;border:1px solid #30363d;color:#e6edf3;padding:9px;border-radius:6px;font-family:inherit;font-size:13px;resize:vertical;min-height:72px;outline:none}
19
+ textarea:focus{border-color:#58a6ff}
20
+ .hint{color:#6e7681;font-size:12px;margin-top:10px}
21
+ </style></head><body>
22
+ <h2>${esc(question)}</h2>
23
+ <div class="plan"><p>${mdToHtml(plan)}</p></div>
24
+ ${diff ? `<div class="diff-wrap"><div class="diff-hdr"><span>Changes</span><div class="diff-stats"><span class="stat-add">+${addCount}</span><span class="stat-del">โˆ’${delCount}</span></div></div><pre class="diff-pre">${diffHtml}</pre></div>` : ""}
25
+ <form method="POST" id="f">
26
+ <input type="hidden" name="choice" id="c" value="Looks good">
27
+ <div class="actions">
28
+ <button class="btn-c" type="submit">โœ… Looks good โ€” move to next offender</button>
29
+ <button class="btn-chat" type="button" onclick="toggleChat()">๐Ÿ’ฌ Request changes</button>
30
+ </div>
31
+ <div class="chat-area" id="ca"><textarea name="freeText" placeholder="Describe what you'd like changed..."></textarea>
32
+ <div style="margin-top:8px"><button class="btn-c" type="submit" onclick="document.getElementById('c').value='Redo'">Submit</button></div></div>
33
+ </form>
34
+ <p class="hint">Tab closes after submit ยท Ctrl+Enter to confirm</p>
35
+ <script>
36
+ function toggleChat(){const r=document.getElementById('ca');r.style.display=r.style.display==='none'?'block':'none';if(r.style.display==='block')r.querySelector('textarea').focus();}
37
+ document.addEventListener('keydown',e=>{if((e.ctrlKey||e.metaKey)&&e.key==='Enter'){document.getElementById('f').submit();}});
38
+ </script>
39
+ </body></html>`;
40
+ export const INTERVIEW_HTML = (question, optionsHtml, hasFreeText, esc) => `<!DOCTYPE html><html lang="en"><head><meta charset="UTF-8"><meta name="viewport" content="width=device-width,initial-scale=1">
41
+ <title>๐Ÿ—๏ธ Decision</title>
42
+ <style>
43
+ *{box-sizing:border-box;margin:0;padding:0}body{font-family:-apple-system,BlinkMacSystemFont,'Segoe UI',sans-serif;background:#0d1117;color:#e6edf3;padding:28px 32px;max-width:880px;margin:0 auto;line-height:1.5}
44
+ .question{font-size:15px;font-weight:600;color:#f0f6fc;margin-bottom:12px}
45
+ .opts{display:flex;flex-direction:column;gap:8px;margin-bottom:16px}
46
+ .card{border:1px solid #30363d;border-radius:8px;padding:11px 14px;cursor:pointer;transition:border-color .12s,background .12s;display:flex;align-items:flex-start;gap:10px}
47
+ .card:hover,.card.selected{border-color:#58a6ff;background:#0d1f30}.card.rec{border-color:#1f6feb}
48
+ .card input{margin-top:3px;accent-color:#58a6ff;flex-shrink:0}.card-body{flex:1}
49
+ .card-top{display:flex;align-items:center;gap:6px;flex-wrap:wrap}
50
+ .num{color:#6e7681;font-size:13px;min-width:18px}.lbl{font-size:13.5px;font-weight:500}
51
+ .badge-rec{background:#1f4e2e;color:#3fb950;font-size:10px;padding:1px 7px;border-radius:10px;margin-left:4px;font-weight:600}
52
+ .ctx{color:#8b949e;font-size:12px;margin-top:3px;padding-left:22px}
53
+ .impact{display:flex;gap:6px;margin-top:5px;padding-left:22px;flex-wrap:wrap}
54
+ .ib{font-size:11px;padding:2px 8px;border-radius:10px;font-family:monospace;font-weight:600}
55
+ .ib.up{background:#1a3a2a;color:#3fb950;border:1px solid #238636}
56
+ .ib.dn{background:#3a1a1a;color:#ff7b72;border:1px solid #f85149}
57
+ .ib.proj{background:#1a2a3a;color:#79c0ff;border:1px solid #1f6feb}
58
+ .free-area{display:none;margin-top:10px;padding-left:22px}
59
+ textarea{width:100%;background:#161b22;border:1px solid #30363d;color:#e6edf3;padding:9px;border-radius:6px;font-family:inherit;font-size:13px;resize:vertical;min-height:72px;outline:none}
60
+ textarea:focus{border-color:#58a6ff}
61
+ .submit-row{display:flex;align-items:center;gap:12px;margin-top:4px}
62
+ button{background:#238636;color:#fff;border:1px solid #2ea043;padding:9px 22px;border-radius:6px;font-size:13.5px;font-weight:600;cursor:pointer;transition:background .12s}
63
+ button:hover{background:#2ea043}.hint{color:#6e7681;font-size:12px}
64
+ </style></head><body>
65
+ <div class="question">${esc(question)}</div>
66
+ <form method="POST" id="f">
67
+ <div class="opts">${optionsHtml}</div>
68
+ ${hasFreeText ? '<div class="free-area" id="fa"><textarea name="freeText" placeholder="Describe your preferred approach..."></textarea></div>' : ""}
69
+ <div class="submit-row"><button type="submit">Submit</button><span class="hint">Ctrl+Enter</span></div>
70
+ </form>
71
+ <script>
72
+ const cards=document.querySelectorAll('.card');function sel(c){cards.forEach(x=>{x.classList.remove('selected');x.querySelector('input').checked=false});c.classList.add('selected');c.querySelector('input').checked=true;const fa=document.getElementById('fa');if(fa)fa.style.display=c.querySelector('input').value==='__free__'?'block':'none';}
73
+ cards.forEach(c=>c.addEventListener('click',()=>sel(c)));const rec=document.querySelector('.card.rec');if(rec)sel(rec);else if(cards.length)sel(cards[0]);
74
+ document.addEventListener('keydown',e=>{if((e.ctrlKey||e.metaKey)&&e.key==='Enter')document.getElementById('f').submit();});
75
+ </script></body></html>`;
@@ -0,0 +1,90 @@
1
+ export const CONFIRMATION_HTML = (
2
+ question: string,
3
+ plan: string,
4
+ diff: string,
5
+ esc: (s: string) => string,
6
+ mdToHtml: (md: string) => string,
7
+ diffHtml: string,
8
+ addCount: number,
9
+ delCount: number,
10
+ ): string => `<!DOCTYPE html><html lang="en"><head><meta charset="UTF-8"><meta name="viewport" content="width=device-width,initial-scale=1">
11
+ <title>๐Ÿ—๏ธ Changes Applied</title>
12
+ <style>
13
+ *{box-sizing:border-box;margin:0;padding:0}body{font-family:-apple-system,BlinkMacSystemFont,'Segoe UI',sans-serif;background:#0d1117;color:#e6edf3;padding:28px 32px;max-width:960px;margin:0 auto;line-height:1.5}
14
+ h2{font-size:16px;color:#58a6ff;margin-bottom:14px}
15
+ .plan{background:#161b22;border:1px solid #30363d;border-radius:8px;padding:14px 18px;margin-bottom:18px;font-size:13px;line-height:1.6}
16
+ .plan h3{color:#f0f6fc;font-size:14px;margin:10px 0 4px}.plan h4{color:#c9d1d9;font-size:13px;margin:8px 0 3px}
17
+ .plan li{margin:2px 0 2px 16px;list-style:disc}.plan code{background:#21262d;padding:1px 5px;border-radius:3px;font-size:12px}
18
+ .diff-wrap{background:#161b22;border:1px solid #30363d;border-radius:8px;margin-bottom:18px;overflow:hidden}
19
+ .diff-hdr{padding:7px 14px;font-size:11px;color:#8b949e;border-bottom:1px solid #30363d;font-family:monospace;display:flex;justify-content:space-between}
20
+ .diff-stats{display:flex;gap:10px}.stat-add{color:#3fb950}.stat-del{color:#ff7b72}
21
+ .diff-pre{padding:12px;font-family:'Fira Code',Consolas,monospace;font-size:12px;line-height:1.55;overflow-x:auto;white-space:pre;margin:0}
22
+ .da{color:#3fb950;display:block}.dd{color:#ff7b72;display:block}.dh{color:#79c0ff;display:block}.df{color:#8b949e;display:block}.dc{color:#e6edf3;display:block}
23
+ .actions{display:flex;gap:10px;flex-wrap:wrap}
24
+ .btn-c{background:#238636;color:#fff;border:1px solid #2ea043;padding:10px 24px;border-radius:6px;font-size:14px;font-weight:600;cursor:pointer}.btn-c:hover{background:#2ea043}
25
+ .btn-chat{background:#1a2332;color:#79c0ff;border:1px solid #1f6feb;padding:10px 24px;border-radius:6px;font-size:14px;cursor:pointer}.btn-chat:hover{background:#1f3050}
26
+ .chat-area{display:none;margin-top:12px}
27
+ textarea{width:100%;background:#161b22;border:1px solid #30363d;color:#e6edf3;padding:9px;border-radius:6px;font-family:inherit;font-size:13px;resize:vertical;min-height:72px;outline:none}
28
+ textarea:focus{border-color:#58a6ff}
29
+ .hint{color:#6e7681;font-size:12px;margin-top:10px}
30
+ </style></head><body>
31
+ <h2>${esc(question)}</h2>
32
+ <div class="plan"><p>${mdToHtml(plan)}</p></div>
33
+ ${diff ? `<div class="diff-wrap"><div class="diff-hdr"><span>Changes</span><div class="diff-stats"><span class="stat-add">+${addCount}</span><span class="stat-del">โˆ’${delCount}</span></div></div><pre class="diff-pre">${diffHtml}</pre></div>` : ""}
34
+ <form method="POST" id="f">
35
+ <input type="hidden" name="choice" id="c" value="Looks good">
36
+ <div class="actions">
37
+ <button class="btn-c" type="submit">โœ… Looks good โ€” move to next offender</button>
38
+ <button class="btn-chat" type="button" onclick="toggleChat()">๐Ÿ’ฌ Request changes</button>
39
+ </div>
40
+ <div class="chat-area" id="ca"><textarea name="freeText" placeholder="Describe what you'd like changed..."></textarea>
41
+ <div style="margin-top:8px"><button class="btn-c" type="submit" onclick="document.getElementById('c').value='Redo'">Submit</button></div></div>
42
+ </form>
43
+ <p class="hint">Tab closes after submit ยท Ctrl+Enter to confirm</p>
44
+ <script>
45
+ function toggleChat(){const r=document.getElementById('ca');r.style.display=r.style.display==='none'?'block':'none';if(r.style.display==='block')r.querySelector('textarea').focus();}
46
+ document.addEventListener('keydown',e=>{if((e.ctrlKey||e.metaKey)&&e.key==='Enter'){document.getElementById('f').submit();}});
47
+ </script>
48
+ </body></html>`;
49
+
50
+ export const INTERVIEW_HTML = (
51
+ question: string,
52
+ optionsHtml: string,
53
+ hasFreeText: boolean,
54
+ esc: (s: string) => string,
55
+ ): string => `<!DOCTYPE html><html lang="en"><head><meta charset="UTF-8"><meta name="viewport" content="width=device-width,initial-scale=1">
56
+ <title>๐Ÿ—๏ธ Decision</title>
57
+ <style>
58
+ *{box-sizing:border-box;margin:0;padding:0}body{font-family:-apple-system,BlinkMacSystemFont,'Segoe UI',sans-serif;background:#0d1117;color:#e6edf3;padding:28px 32px;max-width:880px;margin:0 auto;line-height:1.5}
59
+ .question{font-size:15px;font-weight:600;color:#f0f6fc;margin-bottom:12px}
60
+ .opts{display:flex;flex-direction:column;gap:8px;margin-bottom:16px}
61
+ .card{border:1px solid #30363d;border-radius:8px;padding:11px 14px;cursor:pointer;transition:border-color .12s,background .12s;display:flex;align-items:flex-start;gap:10px}
62
+ .card:hover,.card.selected{border-color:#58a6ff;background:#0d1f30}.card.rec{border-color:#1f6feb}
63
+ .card input{margin-top:3px;accent-color:#58a6ff;flex-shrink:0}.card-body{flex:1}
64
+ .card-top{display:flex;align-items:center;gap:6px;flex-wrap:wrap}
65
+ .num{color:#6e7681;font-size:13px;min-width:18px}.lbl{font-size:13.5px;font-weight:500}
66
+ .badge-rec{background:#1f4e2e;color:#3fb950;font-size:10px;padding:1px 7px;border-radius:10px;margin-left:4px;font-weight:600}
67
+ .ctx{color:#8b949e;font-size:12px;margin-top:3px;padding-left:22px}
68
+ .impact{display:flex;gap:6px;margin-top:5px;padding-left:22px;flex-wrap:wrap}
69
+ .ib{font-size:11px;padding:2px 8px;border-radius:10px;font-family:monospace;font-weight:600}
70
+ .ib.up{background:#1a3a2a;color:#3fb950;border:1px solid #238636}
71
+ .ib.dn{background:#3a1a1a;color:#ff7b72;border:1px solid #f85149}
72
+ .ib.proj{background:#1a2a3a;color:#79c0ff;border:1px solid #1f6feb}
73
+ .free-area{display:none;margin-top:10px;padding-left:22px}
74
+ textarea{width:100%;background:#161b22;border:1px solid #30363d;color:#e6edf3;padding:9px;border-radius:6px;font-family:inherit;font-size:13px;resize:vertical;min-height:72px;outline:none}
75
+ textarea:focus{border-color:#58a6ff}
76
+ .submit-row{display:flex;align-items:center;gap:12px;margin-top:4px}
77
+ button{background:#238636;color:#fff;border:1px solid #2ea043;padding:9px 22px;border-radius:6px;font-size:13.5px;font-weight:600;cursor:pointer;transition:background .12s}
78
+ button:hover{background:#2ea043}.hint{color:#6e7681;font-size:12px}
79
+ </style></head><body>
80
+ <div class="question">${esc(question)}</div>
81
+ <form method="POST" id="f">
82
+ <div class="opts">${optionsHtml}</div>
83
+ ${hasFreeText ? '<div class="free-area" id="fa"><textarea name="freeText" placeholder="Describe your preferred approach..."></textarea></div>' : ""}
84
+ <div class="submit-row"><button type="submit">Submit</button><span class="hint">Ctrl+Enter</span></div>
85
+ </form>
86
+ <script>
87
+ const cards=document.querySelectorAll('.card');function sel(c){cards.forEach(x=>{x.classList.remove('selected');x.querySelector('input').checked=false});c.classList.add('selected');c.querySelector('input').checked=true;const fa=document.getElementById('fa');if(fa)fa.style.display=c.querySelector('input').value==='__free__'?'block':'none';}
88
+ cards.forEach(c=>c.addEventListener('click',()=>sel(c)));const rec=document.querySelector('.card.rec');if(rec)sel(rec);else if(cards.length)sel(cards[0]);
89
+ document.addEventListener('keydown',e=>{if((e.ctrlKey||e.metaKey)&&e.key==='Enter')document.getElementById('f').submit();});
90
+ </script></body></html>`;
@@ -1,14 +1,27 @@
1
+ import { spawnSync } from "node:child_process";
2
+ import * as http from "node:http";
3
+ import * as net from "node:net";
1
4
  import { Type } from "@sinclair/typebox";
2
- export function buildInterviewer(pi, dbg) {
5
+ import { CONFIRMATION_HTML, INTERVIEW_HTML } from "./interviewer-templates.js";
6
+ export function buildInterviewer(pi, _dbg) {
3
7
  let interviewHandler = null;
8
+ const esc = (s) => s
9
+ .replace(/&/g, "&amp;")
10
+ .replace(/</g, "&lt;")
11
+ .replace(/>/g, "&gt;")
12
+ .replace(/"/g, "&quot;");
13
+ const mdToHtml = (md) => md
14
+ .replace(/\*\*(.+?)\*\*/g, "<strong>$1</strong>")
15
+ .replace(/`([^`]+)`/g, "<code>$1</code>")
16
+ .replace(/^### (.+)/gm, "<h4>$1</h4>")
17
+ .replace(/^## (.+)/gm, "<h3>$1</h3>")
18
+ .replace(/^# (.+)/gm, "<h2>$1</h2>")
19
+ .replace(/^- (.+)/gm, "<li>$1</li>")
20
+ .replace(/\n\n/g, "</p><p>");
4
21
  const confirmationHTML = (question, plan, diff) => {
5
- const esc = (s) => s.replace(/&/g, "&amp;").replace(/</g, "&lt;").replace(/>/g, "&gt;").replace(/"/g, "&quot;");
6
- const mdToHtml = (md) => md.replace(/\*\*(.+?)\*\*/g, "<strong>$1</strong>").replace(/`([^`]+)`/g, "<code>$1</code>")
7
- .replace(/^### (.+)/gm, "<h4>$1</h4>").replace(/^## (.+)/gm, "<h3>$1</h3>")
8
- .replace(/^# (.+)/gm, "<h2>$1</h2>").replace(/^- (.+)/gm, "<li>$1</li>")
9
- .replace(/\n\n/g, "</p><p>");
10
22
  const diffLines = diff.split("\n");
11
- const diffHtml = diffLines.map((line) => {
23
+ const diffHtml = diffLines
24
+ .map((line) => {
12
25
  if (line.startsWith("+++") || line.startsWith("---"))
13
26
  return `<span class="df">${esc(line)}</span>`;
14
27
  if (line.startsWith("@@"))
@@ -18,54 +31,17 @@ export function buildInterviewer(pi, dbg) {
18
31
  if (line.startsWith("-"))
19
32
  return `<span class="dd">${esc(line)}</span>`;
20
33
  return `<span class="dc">${esc(line)}</span>`;
21
- }).join("\n");
34
+ })
35
+ .join("\n");
22
36
  const addCount = (diff.match(/^\+/gm) || []).length;
23
37
  const delCount = (diff.match(/^-/gm) || []).length - (diff.match(/^---/gm) || []).length;
24
- return `<!DOCTYPE html><html lang="en"><head><meta charset="UTF-8"><meta name="viewport" content="width=device-width,initial-scale=1">
25
- <title>๐Ÿ—๏ธ Changes Applied</title>
26
- <style>
27
- *{box-sizing:border-box;margin:0;padding:0}body{font-family:-apple-system,BlinkMacSystemFont,'Segoe UI',sans-serif;background:#0d1117;color:#e6edf3;padding:28px 32px;max-width:960px;margin:0 auto;line-height:1.5}
28
- h2{font-size:16px;color:#58a6ff;margin-bottom:14px}
29
- .plan{background:#161b22;border:1px solid #30363d;border-radius:8px;padding:14px 18px;margin-bottom:18px;font-size:13px;line-height:1.6}
30
- .plan h3{color:#f0f6fc;font-size:14px;margin:10px 0 4px}.plan h4{color:#c9d1d9;font-size:13px;margin:8px 0 3px}
31
- .plan li{margin:2px 0 2px 16px;list-style:disc}.plan code{background:#21262d;padding:1px 5px;border-radius:3px;font-size:12px}
32
- .diff-wrap{background:#161b22;border:1px solid #30363d;border-radius:8px;margin-bottom:18px;overflow:hidden}
33
- .diff-hdr{padding:7px 14px;font-size:11px;color:#8b949e;border-bottom:1px solid #30363d;font-family:monospace;display:flex;justify-content:space-between}
34
- .diff-stats{display:flex;gap:10px}.stat-add{color:#3fb950}.stat-del{color:#ff7b72}
35
- .diff-pre{padding:12px;font-family:'Fira Code',Consolas,monospace;font-size:12px;line-height:1.55;overflow-x:auto;white-space:pre;margin:0}
36
- .da{color:#3fb950;display:block}.dd{color:#ff7b72;display:block}.dh{color:#79c0ff;display:block}.df{color:#8b949e;display:block}.dc{color:#e6edf3;display:block}
37
- .actions{display:flex;gap:10px;flex-wrap:wrap}
38
- .btn-c{background:#238636;color:#fff;border:1px solid #2ea043;padding:10px 24px;border-radius:6px;font-size:14px;font-weight:600;cursor:pointer}.btn-c:hover{background:#2ea043}
39
- .btn-chat{background:#1a2332;color:#79c0ff;border:1px solid #1f6feb;padding:10px 24px;border-radius:6px;font-size:14px;cursor:pointer}.btn-chat:hover{background:#1f3050}
40
- .chat-area{display:none;margin-top:12px}
41
- textarea{width:100%;background:#161b22;border:1px solid #30363d;color:#e6edf3;padding:9px;border-radius:6px;font-family:inherit;font-size:13px;resize:vertical;min-height:72px;outline:none}
42
- textarea:focus{border-color:#58a6ff}
43
- .hint{color:#6e7681;font-size:12px;margin-top:10px}
44
- </style></head><body>
45
- <h2>${esc(question)}</h2>
46
- <div class="plan"><p>${mdToHtml(plan)}</p></div>
47
- ${diff ? `<div class="diff-wrap"><div class="diff-hdr"><span>Changes</span><div class="diff-stats"><span class="stat-add">+${addCount}</span><span class="stat-del">โˆ’${delCount}</span></div></div><pre class="diff-pre">${diffHtml}</pre></div>` : ""}
48
- <form method="POST" id="f">
49
- <input type="hidden" name="choice" id="c" value="Looks good">
50
- <div class="actions">
51
- <button class="btn-c" type="submit">โœ… Looks good โ€” move to next offender</button>
52
- <button class="btn-chat" type="button" onclick="toggleChat()">๐Ÿ’ฌ Request changes</button>
53
- </div>
54
- <div class="chat-area" id="ca"><textarea name="freeText" placeholder="Describe what you'd like changed..."></textarea>
55
- <div style="margin-top:8px"><button class="btn-c" type="submit" onclick="document.getElementById('c').value='Redo'">Submit</button></div></div>
56
- </form>
57
- <p class="hint">Tab closes after submit ยท Ctrl+Enter to confirm</p>
58
- <script>
59
- function toggleChat(){const r=document.getElementById('ca');r.style.display=r.style.display==='none'?'block':'none';if(r.style.display==='block')r.querySelector('textarea').focus();}
60
- document.addEventListener('keydown',e=>{if((e.ctrlKey||e.metaKey)&&e.key==='Enter'){document.getElementById('f').submit();}});
61
- </script>
62
- </body></html>`;
38
+ return CONFIRMATION_HTML(question, plan, diff, esc, mdToHtml, diffHtml, addCount, delCount);
63
39
  };
64
40
  const interviewHTML = (question, options, _timeoutSeconds, _plan, _diff, _confirmationMode) => {
65
41
  if (_confirmationMode && _plan && _diff)
66
42
  return confirmationHTML(question, _plan, _diff);
67
- const esc = (s) => s.replace(/&/g, "&amp;").replace(/</g, "&lt;").replace(/>/g, "&gt;").replace(/"/g, "&quot;");
68
- const optionsHtml = options.map((opt, idx) => {
43
+ const optionsHtml = options
44
+ .map((opt, idx) => {
69
45
  const impactBadge = (val, label, good) => `<span class="ib ${good ? "up" : "dn"}">${val > 0 ? "+" : ""}${val} ${label}</span>`;
70
46
  let impactHtml = "";
71
47
  if (opt.impact) {
@@ -80,52 +56,19 @@ document.addEventListener('keydown',e=>{if((e.ctrlKey||e.metaKey)&&e.key==='Ente
80
56
  impactHtml = `<div class="impact">${parts.join("")}</div>`;
81
57
  }
82
58
  return `<label class="card${opt.recommended ? " rec" : ""}"><input type="radio" name="choice" value="${esc(opt.value)}"${opt.recommended ? " checked" : ""}><div class="card-body"><div class="card-top"><span class="num">${idx + 1}.</span><span class="lbl">${esc(opt.label)}</span>${opt.recommended ? '<span class="badge-rec">Recommended</span>' : ""}</div>${impactHtml}${opt.context ? `<div class="ctx">${esc(opt.context)}</div>` : ""}</div></label>`;
83
- }).join("\n");
59
+ })
60
+ .join("\n");
84
61
  const hasFreeText = options.some((o) => o.value === "__free__");
85
- return `<!DOCTYPE html><html lang="en"><head><meta charset="UTF-8"><meta name="viewport" content="width=device-width,initial-scale=1">
86
- <title>๐Ÿ—๏ธ Decision</title>
87
- <style>
88
- *{box-sizing:border-box;margin:0;padding:0}body{font-family:-apple-system,BlinkMacSystemFont,'Segoe UI',sans-serif;background:#0d1117;color:#e6edf3;padding:28px 32px;max-width:880px;margin:0 auto;line-height:1.5}
89
- .question{font-size:15px;font-weight:600;color:#f0f6fc;margin-bottom:12px}
90
- .opts{display:flex;flex-direction:column;gap:8px;margin-bottom:16px}
91
- .card{border:1px solid #30363d;border-radius:8px;padding:11px 14px;cursor:pointer;transition:border-color .12s,background .12s;display:flex;align-items:flex-start;gap:10px}
92
- .card:hover,.card.selected{border-color:#58a6ff;background:#0d1f30}.card.rec{border-color:#1f6feb}
93
- .card input{margin-top:3px;accent-color:#58a6ff;flex-shrink:0}.card-body{flex:1}
94
- .card-top{display:flex;align-items:center;gap:6px;flex-wrap:wrap}
95
- .num{color:#6e7681;font-size:13px;min-width:18px}.lbl{font-size:13.5px;font-weight:500}
96
- .badge-rec{background:#1f4e2e;color:#3fb950;font-size:10px;padding:1px 7px;border-radius:10px;margin-left:4px;font-weight:600}
97
- .ctx{color:#8b949e;font-size:12px;margin-top:3px;padding-left:22px}
98
- .impact{display:flex;gap:6px;margin-top:5px;padding-left:22px;flex-wrap:wrap}
99
- .ib{font-size:11px;padding:2px 8px;border-radius:10px;font-family:monospace;font-weight:600}
100
- .ib.up{background:#1a3a2a;color:#3fb950;border:1px solid #238636}
101
- .ib.dn{background:#3a1a1a;color:#ff7b72;border:1px solid #f85149}
102
- .ib.proj{background:#1a2a3a;color:#79c0ff;border:1px solid #1f6feb}
103
- .free-area{display:none;margin-top:10px;padding-left:22px}
104
- textarea{width:100%;background:#161b22;border:1px solid #30363d;color:#e6edf3;padding:9px;border-radius:6px;font-family:inherit;font-size:13px;resize:vertical;min-height:72px;outline:none}
105
- textarea:focus{border-color:#58a6ff}
106
- .submit-row{display:flex;align-items:center;gap:12px;margin-top:4px}
107
- button{background:#238636;color:#fff;border:1px solid #2ea043;padding:9px 22px;border-radius:6px;font-size:13.5px;font-weight:600;cursor:pointer;transition:background .12s}
108
- button:hover{background:#2ea043}.hint{color:#6e7681;font-size:12px}
109
- </style></head><body>
110
- <div class="question">${esc(question)}</div>
111
- <form method="POST" id="f">
112
- <div class="opts">${optionsHtml}</div>
113
- ${hasFreeText ? '<div class="free-area" id="fa"><textarea name="freeText" placeholder="Describe your preferred approach..."></textarea></div>' : ""}
114
- <div class="submit-row"><button type="submit">Submit</button><span class="hint">Ctrl+Enter</span></div>
115
- </form>
116
- <script>
117
- const cards=document.querySelectorAll('.card');function sel(c){cards.forEach(x=>{x.classList.remove('selected');x.querySelector('input').checked=false});c.classList.add('selected');c.querySelector('input').checked=true;const fa=document.getElementById('fa');if(fa)fa.style.display=c.querySelector('input').value==='__free__'?'block':'none';}
118
- cards.forEach(c=>c.addEventListener('click',()=>sel(c)));const rec=document.querySelector('.card.rec');if(rec)sel(rec);else if(cards.length)sel(cards[0]);
119
- document.addEventListener('keydown',e=>{if((e.ctrlKey||e.metaKey)&&e.key==='Enter')document.getElementById('f').submit();});
120
- </script></body></html>`;
62
+ return INTERVIEW_HTML(question, optionsHtml, hasFreeText, esc);
121
63
  };
122
64
  const openBrowserInterview = (html, timeoutSeconds) => {
123
- const http = require("node:http");
124
- const net = require("node:net");
125
65
  return new Promise((resolve) => {
126
66
  const getPort = (cb) => {
127
67
  const s = net.createServer();
128
- s.listen(0, () => { const p = s.address().port; s.close(() => cb(p)); });
68
+ s.listen(0, () => {
69
+ const p = s.address().port;
70
+ s.close(() => cb(p));
71
+ });
129
72
  s.on("error", () => cb(-1));
130
73
  };
131
74
  getPort((port) => {
@@ -140,14 +83,20 @@ document.addEventListener('keydown',e=>{if((e.ctrlKey||e.metaKey)&&e.key==='Ente
140
83
  }
141
84
  else if (req.method === "POST") {
142
85
  let body = "";
143
- req.on("data", (c) => { body += c.toString(); });
86
+ req.on("data", (c) => {
87
+ body += c.toString();
88
+ });
144
89
  req.on("end", () => {
145
90
  const p = new URLSearchParams(body);
146
91
  const choice = p.get("choice") ?? "";
147
92
  const freeText = p.get("freeText") ?? "";
148
- const final = choice === "__free__" || choice === "Redo" ? freeText.trim() : choice;
149
- res.writeHead(200, { "Content-Type": "text/html; charset=utf-8" });
150
- res.end("<!DOCTYPE html><html><head><meta charset='UTF-8'><style>body{font-family:system-ui;background:#0d1117;color:#e6edf3;display:flex;align-items:center;justify-content:center;height:100vh;margin:0;text-align:center}</style></head><body><div><h2>โœ… Response received</h2><p style='color:#8b949e;margin-top:8px'>You can close this tab.</p></div></body></html>");
93
+ const final = choice === "__free__" || choice === "Redo"
94
+ ? freeText.trim()
95
+ : choice;
96
+ res.writeHead(200, {
97
+ "Content-Type": "text/html; charset=utf-8",
98
+ });
99
+ res.end(`<!DOCTYPE html><html><head><meta charset='UTF-8'><style>body{font-family:system-ui;background:#0d1117;color:#e6edf3;display:flex;align-items:center;justify-content:center;height:100vh;margin:0;text-align:center}.fade{transition:opacity 0.5s}</style></head><body><div class="fade"><h2>โœ… Response received</h2><p style='color:#8b949e;margin-top:8px'>Closing tab...</p><p id="count" style='color:#58a6ff;margin-top:4px'></p></div><script>let s=3;const el=document.getElementById('count');const tick=()=>{el.textContent=s+'s';if(s<=0){window.close();}else{s--;setTimeout(tick,1000);}};tick();</script></body></html>`);
151
100
  clearTimeout(timer);
152
101
  server.close();
153
102
  resolve(final || null);
@@ -155,7 +104,6 @@ document.addEventListener('keydown',e=>{if((e.ctrlKey||e.metaKey)&&e.key==='Ente
155
104
  }
156
105
  });
157
106
  server.listen(port);
158
- const { spawnSync } = require("node:child_process");
159
107
  const url = `http://localhost:${port}`;
160
108
  if (process.platform === "win32")
161
109
  spawnSync("cmd", ["/c", "start", "", url], { shell: false });
@@ -163,7 +111,10 @@ document.addEventListener('keydown',e=>{if((e.ctrlKey||e.metaKey)&&e.key==='Ente
163
111
  spawnSync("open", [url]);
164
112
  else
165
113
  spawnSync("xdg-open", [url]);
166
- const timer = setTimeout(() => { server.close(); resolve(null); }, timeoutSeconds * 1000);
114
+ const timer = setTimeout(() => {
115
+ server.close();
116
+ resolve(null);
117
+ }, timeoutSeconds * 1000);
167
118
  });
168
119
  });
169
120
  };
@@ -173,7 +124,9 @@ document.addEventListener('keydown',e=>{if((e.ctrlKey||e.metaKey)&&e.key==='Ente
173
124
  label: "Interview",
174
125
  description: "Present a multiple-choice interview to the user via browser form. Use this when you need the user to make a decision with options. Returns their choice or null on timeout. Supports confirmation mode with plan+diff display.",
175
126
  parameters: Type.Object({
176
- question: Type.String({ description: "The question to present to the user" }),
127
+ question: Type.String({
128
+ description: "The question to present to the user",
129
+ }),
177
130
  options: Type.Optional(Type.Array(Type.Object({
178
131
  value: Type.String(),
179
132
  label: Type.String(),
@@ -185,16 +138,35 @@ document.addEventListener('keydown',e=>{if((e.ctrlKey||e.metaKey)&&e.key==='Ente
185
138
  cognitiveProjection: Type.Optional(Type.String()),
186
139
  })),
187
140
  }))),
188
- plan: Type.Optional(Type.String({ description: "Refactoring plan (markdown) โ€” shows in confirmation mode" })),
189
- diff: Type.Optional(Type.String({ description: "Unified diff text โ€” shows in confirmation mode" })),
141
+ plan: Type.Optional(Type.String({
142
+ description: "Refactoring plan (markdown) โ€” shows in confirmation mode",
143
+ })),
144
+ diff: Type.Optional(Type.String({
145
+ description: "Unified diff text โ€” shows in confirmation mode",
146
+ })),
190
147
  confirmationMode: Type.Optional(Type.Boolean({ description: "Show plan+diff confirmation screen" })),
191
- timeoutSeconds: Type.Optional(Type.Number({ description: "Auto-close after this many seconds (default 600)" })),
148
+ timeoutSeconds: Type.Optional(Type.Number({
149
+ description: "Auto-close after this many seconds (default 600)",
150
+ })),
192
151
  }),
193
152
  async execute(_toolCallId, input, _signal, _onUpdate, _ctx) {
194
153
  if (!interviewHandler)
195
- return { content: [{ type: "text", text: "Interview tool not initialized" }], details: null };
154
+ return {
155
+ content: [
156
+ { type: "text", text: "Interview tool not initialized" },
157
+ ],
158
+ details: null,
159
+ };
196
160
  const result = await interviewHandler(input.question, input.options ?? [], input.timeoutSeconds ?? 600, input.plan, input.diff, input.confirmationMode);
197
- return { content: [{ type: "text", text: result ?? "No response (timed out or dismissed)" }], details: result ?? null };
161
+ return {
162
+ content: [
163
+ {
164
+ type: "text",
165
+ text: result ?? "No response (timed out or dismissed)",
166
+ },
167
+ ],
168
+ details: result ?? null,
169
+ };
198
170
  },
199
171
  });
200
172
  return interviewHandler;