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
@@ -1,14 +1,9 @@
1
- /**
2
- * Generic interviewer tool — browser-based multiple-choice + confirmation with diff.
3
- * Used by lens-booboo-refactor for interactive architectural decisions.
4
- *
5
- * Two modes:
6
- * 1. Option selection: question + options with impact metrics
7
- * 2. Confirmation: plan + diff with confirm/cancel/redo
8
- */
9
- import * as path from "node:path";
10
- import { Type } from "@sinclair/typebox";
1
+ import { spawnSync } from "node:child_process";
2
+ import * as http from "node:http";
3
+ import * as net from "node:net";
11
4
  import type { ExtensionAPI } from "@mariozechner/pi-coding-agent";
5
+ import { Type } from "@sinclair/typebox";
6
+ import { CONFIRMATION_HTML, INTERVIEW_HTML } from "./interviewer-templates.js";
12
7
 
13
8
  export type InterviewOption = {
14
9
  value: string;
@@ -24,147 +19,156 @@ export type InterviewOption = {
24
19
 
25
20
  export function buildInterviewer(
26
21
  pi: ExtensionAPI,
27
- dbg: (msg: string) => void,
28
- ): (question: string, options: InterviewOption[], timeoutSeconds: number, plan?: string, diff?: string, confirmationMode?: boolean) => Promise<string | null> {
29
- let interviewHandler: ((question: string, options: InterviewOption[], timeoutSeconds: number, plan?: string, diff?: string, confirmationMode?: boolean) => Promise<string | null>) | null = null;
22
+ _dbg: (msg: string) => void,
23
+ ): (
24
+ question: string,
25
+ options: InterviewOption[],
26
+ timeoutSeconds: number,
27
+ plan?: string,
28
+ diff?: string,
29
+ confirmationMode?: boolean,
30
+ ) => Promise<string | null> {
31
+ let interviewHandler:
32
+ | ((
33
+ question: string,
34
+ options: InterviewOption[],
35
+ timeoutSeconds: number,
36
+ plan?: string,
37
+ diff?: string,
38
+ confirmationMode?: boolean,
39
+ ) => Promise<string | null>)
40
+ | null = null;
41
+
42
+ const esc = (s: string) =>
43
+ s
44
+ .replace(/&/g, "&amp;")
45
+ .replace(/</g, "&lt;")
46
+ .replace(/>/g, "&gt;")
47
+ .replace(/"/g, "&quot;");
48
+
49
+ const mdToHtml = (md: string) =>
50
+ md
51
+ .replace(/\*\*(.+?)\*\*/g, "<strong>$1</strong>")
52
+ .replace(/`([^`]+)`/g, "<code>$1</code>")
53
+ .replace(/^### (.+)/gm, "<h4>$1</h4>")
54
+ .replace(/^## (.+)/gm, "<h3>$1</h3>")
55
+ .replace(/^# (.+)/gm, "<h2>$1</h2>")
56
+ .replace(/^- (.+)/gm, "<li>$1</li>")
57
+ .replace(/\n\n/g, "</p><p>");
30
58
 
31
- const confirmationHTML = (question: string, plan: string, diff: string): string => {
32
- const esc = (s: string) => s.replace(/&/g, "&amp;").replace(/</g, "&lt;").replace(/>/g, "&gt;").replace(/"/g, "&quot;");
33
- const mdToHtml = (md: string) =>
34
- md.replace(/\*\*(.+?)\*\*/g, "<strong>$1</strong>").replace(/`([^`]+)`/g, "<code>$1</code>")
35
- .replace(/^### (.+)/gm, "<h4>$1</h4>").replace(/^## (.+)/gm, "<h3>$1</h3>")
36
- .replace(/^# (.+)/gm, "<h2>$1</h2>").replace(/^- (.+)/gm, "<li>$1</li>")
37
- .replace(/\n\n/g, "</p><p>");
59
+ const confirmationHTML = (
60
+ question: string,
61
+ plan: string,
62
+ diff: string,
63
+ ): string => {
38
64
  const diffLines = diff.split("\n");
39
- const diffHtml = diffLines.map((line) => {
40
- if (line.startsWith("+++") || line.startsWith("---")) return `<span class="df">${esc(line)}</span>`;
41
- if (line.startsWith("@@")) return `<span class="dh">${esc(line)}</span>`;
42
- if (line.startsWith("+")) return `<span class="da">${esc(line)}</span>`;
43
- if (line.startsWith("-")) return `<span class="dd">${esc(line)}</span>`;
44
- return `<span class="dc">${esc(line)}</span>`;
45
- }).join("\n");
65
+ const diffHtml = diffLines
66
+ .map((line) => {
67
+ if (line.startsWith("+++") || line.startsWith("---"))
68
+ return `<span class="df">${esc(line)}</span>`;
69
+ if (line.startsWith("@@"))
70
+ return `<span class="dh">${esc(line)}</span>`;
71
+ if (line.startsWith("+")) return `<span class="da">${esc(line)}</span>`;
72
+ if (line.startsWith("-")) return `<span class="dd">${esc(line)}</span>`;
73
+ return `<span class="dc">${esc(line)}</span>`;
74
+ })
75
+ .join("\n");
46
76
  const addCount = (diff.match(/^\+/gm) || []).length;
47
- const delCount = (diff.match(/^-/gm) || []).length - (diff.match(/^---/gm) || []).length;
48
- return `<!DOCTYPE html><html lang="en"><head><meta charset="UTF-8"><meta name="viewport" content="width=device-width,initial-scale=1">
49
- <title>🏗️ Changes Applied</title>
50
- <style>
51
- *{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}
52
- h2{font-size:16px;color:#58a6ff;margin-bottom:14px}
53
- .plan{background:#161b22;border:1px solid #30363d;border-radius:8px;padding:14px 18px;margin-bottom:18px;font-size:13px;line-height:1.6}
54
- .plan h3{color:#f0f6fc;font-size:14px;margin:10px 0 4px}.plan h4{color:#c9d1d9;font-size:13px;margin:8px 0 3px}
55
- .plan li{margin:2px 0 2px 16px;list-style:disc}.plan code{background:#21262d;padding:1px 5px;border-radius:3px;font-size:12px}
56
- .diff-wrap{background:#161b22;border:1px solid #30363d;border-radius:8px;margin-bottom:18px;overflow:hidden}
57
- .diff-hdr{padding:7px 14px;font-size:11px;color:#8b949e;border-bottom:1px solid #30363d;font-family:monospace;display:flex;justify-content:space-between}
58
- .diff-stats{display:flex;gap:10px}.stat-add{color:#3fb950}.stat-del{color:#ff7b72}
59
- .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}
60
- .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}
61
- .actions{display:flex;gap:10px;flex-wrap:wrap}
62
- .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}
63
- .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}
64
- .chat-area{display:none;margin-top:12px}
65
- 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}
66
- textarea:focus{border-color:#58a6ff}
67
- .hint{color:#6e7681;font-size:12px;margin-top:10px}
68
- </style></head><body>
69
- <h2>${esc(question)}</h2>
70
- <div class="plan"><p>${mdToHtml(plan)}</p></div>
71
- ${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>` : ""}
72
- <form method="POST" id="f">
73
- <input type="hidden" name="choice" id="c" value="Looks good">
74
- <div class="actions">
75
- <button class="btn-c" type="submit">✅ Looks good — move to next offender</button>
76
- <button class="btn-chat" type="button" onclick="toggleChat()">💬 Request changes</button>
77
- </div>
78
- <div class="chat-area" id="ca"><textarea name="freeText" placeholder="Describe what you'd like changed..."></textarea>
79
- <div style="margin-top:8px"><button class="btn-c" type="submit" onclick="document.getElementById('c').value='Redo'">Submit</button></div></div>
80
- </form>
81
- <p class="hint">Tab closes after submit · Ctrl+Enter to confirm</p>
82
- <script>
83
- 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();}
84
- document.addEventListener('keydown',e=>{if((e.ctrlKey||e.metaKey)&&e.key==='Enter'){document.getElementById('f').submit();}});
85
- </script>
86
- </body></html>`;
77
+ const delCount =
78
+ (diff.match(/^-/gm) || []).length - (diff.match(/^---/gm) || []).length;
79
+
80
+ return CONFIRMATION_HTML(
81
+ question,
82
+ plan,
83
+ diff,
84
+ esc,
85
+ mdToHtml,
86
+ diffHtml,
87
+ addCount,
88
+ delCount,
89
+ );
87
90
  };
88
91
 
89
- const interviewHTML = (question: string, options: InterviewOption[], _timeoutSeconds: number, _plan?: string, _diff?: string, _confirmationMode?: boolean): string => {
90
- if (_confirmationMode && _plan && _diff) return confirmationHTML(question, _plan, _diff);
91
- const esc = (s: string) => s.replace(/&/g, "&amp;").replace(/</g, "&lt;").replace(/>/g, "&gt;").replace(/"/g, "&quot;");
92
- const optionsHtml = options.map((opt, idx) => {
93
- const impactBadge = (val: number, label: string, good: boolean) => `<span class="ib ${good ? "up" : "dn"}">${val > 0 ? "+" : ""}${val} ${label}</span>`;
94
- let impactHtml = "";
95
- if (opt.impact) {
96
- const parts: string[] = [];
97
- if (opt.impact.linesReduced !== undefined) parts.push(impactBadge(opt.impact.linesReduced, "lines", true));
98
- if (opt.impact.miProjection) parts.push(`<span class="ib proj">MI ${opt.impact.miProjection}</span>`);
99
- if (opt.impact.cognitiveProjection) parts.push(`<span class="ib proj">Cognitive ${opt.impact.cognitiveProjection}</span>`);
100
- if (parts.length) impactHtml = `<div class="impact">${parts.join("")}</div>`;
101
- }
102
- 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>`;
103
- }).join("\n");
92
+ const interviewHTML = (
93
+ question: string,
94
+ options: InterviewOption[],
95
+ _timeoutSeconds: number,
96
+ _plan?: string,
97
+ _diff?: string,
98
+ _confirmationMode?: boolean,
99
+ ): string => {
100
+ if (_confirmationMode && _plan && _diff)
101
+ return confirmationHTML(question, _plan, _diff);
102
+
103
+ const optionsHtml = options
104
+ .map((opt, idx) => {
105
+ const impactBadge = (val: number, label: string, good: boolean) =>
106
+ `<span class="ib ${good ? "up" : "dn"}">${val > 0 ? "+" : ""}${val} ${label}</span>`;
107
+ let impactHtml = "";
108
+ if (opt.impact) {
109
+ const parts: string[] = [];
110
+ if (opt.impact.linesReduced !== undefined)
111
+ parts.push(impactBadge(opt.impact.linesReduced, "lines", true));
112
+ if (opt.impact.miProjection)
113
+ parts.push(
114
+ `<span class="ib proj">MI ${opt.impact.miProjection}</span>`,
115
+ );
116
+ if (opt.impact.cognitiveProjection)
117
+ parts.push(
118
+ `<span class="ib proj">Cognitive ${opt.impact.cognitiveProjection}</span>`,
119
+ );
120
+ if (parts.length)
121
+ impactHtml = `<div class="impact">${parts.join("")}</div>`;
122
+ }
123
+ 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>`;
124
+ })
125
+ .join("\n");
104
126
  const hasFreeText = options.some((o) => o.value === "__free__");
105
- return `<!DOCTYPE html><html lang="en"><head><meta charset="UTF-8"><meta name="viewport" content="width=device-width,initial-scale=1">
106
- <title>🏗️ Decision</title>
107
- <style>
108
- *{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}
109
- .question{font-size:15px;font-weight:600;color:#f0f6fc;margin-bottom:12px}
110
- .opts{display:flex;flex-direction:column;gap:8px;margin-bottom:16px}
111
- .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}
112
- .card:hover,.card.selected{border-color:#58a6ff;background:#0d1f30}.card.rec{border-color:#1f6feb}
113
- .card input{margin-top:3px;accent-color:#58a6ff;flex-shrink:0}.card-body{flex:1}
114
- .card-top{display:flex;align-items:center;gap:6px;flex-wrap:wrap}
115
- .num{color:#6e7681;font-size:13px;min-width:18px}.lbl{font-size:13.5px;font-weight:500}
116
- .badge-rec{background:#1f4e2e;color:#3fb950;font-size:10px;padding:1px 7px;border-radius:10px;margin-left:4px;font-weight:600}
117
- .ctx{color:#8b949e;font-size:12px;margin-top:3px;padding-left:22px}
118
- .impact{display:flex;gap:6px;margin-top:5px;padding-left:22px;flex-wrap:wrap}
119
- .ib{font-size:11px;padding:2px 8px;border-radius:10px;font-family:monospace;font-weight:600}
120
- .ib.up{background:#1a3a2a;color:#3fb950;border:1px solid #238636}
121
- .ib.dn{background:#3a1a1a;color:#ff7b72;border:1px solid #f85149}
122
- .ib.proj{background:#1a2a3a;color:#79c0ff;border:1px solid #1f6feb}
123
- .free-area{display:none;margin-top:10px;padding-left:22px}
124
- 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}
125
- textarea:focus{border-color:#58a6ff}
126
- .submit-row{display:flex;align-items:center;gap:12px;margin-top:4px}
127
- 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}
128
- button:hover{background:#2ea043}.hint{color:#6e7681;font-size:12px}
129
- </style></head><body>
130
- <div class="question">${esc(question)}</div>
131
- <form method="POST" id="f">
132
- <div class="opts">${optionsHtml}</div>
133
- ${hasFreeText ? '<div class="free-area" id="fa"><textarea name="freeText" placeholder="Describe your preferred approach..."></textarea></div>' : ""}
134
- <div class="submit-row"><button type="submit">Submit</button><span class="hint">Ctrl+Enter</span></div>
135
- </form>
136
- <script>
137
- 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';}
138
- 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]);
139
- document.addEventListener('keydown',e=>{if((e.ctrlKey||e.metaKey)&&e.key==='Enter')document.getElementById('f').submit();});
140
- </script></body></html>`;
127
+
128
+ return INTERVIEW_HTML(question, optionsHtml, hasFreeText, esc);
141
129
  };
142
130
 
143
- const openBrowserInterview = (html: string, timeoutSeconds: number): Promise<string | null> => {
144
- const http = require("node:http") as typeof import("node:http");
145
- const net = require("node:net");
131
+ const openBrowserInterview = (
132
+ html: string,
133
+ timeoutSeconds: number,
134
+ ): Promise<string | null> => {
146
135
  return new Promise((resolve) => {
147
136
  const getPort = (cb: (port: number) => void) => {
148
137
  const s = net.createServer();
149
- s.listen(0, () => { const p = (s.address() as { port: number }).port; s.close(() => cb(p)); });
138
+ s.listen(0, () => {
139
+ const p = (s.address() as net.AddressInfo).port;
140
+ s.close(() => cb(p));
141
+ });
150
142
  s.on("error", () => cb(-1));
151
143
  };
152
144
  getPort((port) => {
153
- if (port < 0) { resolve(null); return; }
145
+ if (port < 0) {
146
+ resolve(null);
147
+ return;
148
+ }
154
149
  const server = http.createServer((req, res) => {
155
150
  if (req.method === "GET") {
156
151
  res.writeHead(200, { "Content-Type": "text/html; charset=utf-8" });
157
152
  res.end(html);
158
153
  } else if (req.method === "POST") {
159
154
  let body = "";
160
- req.on("data", (c: Buffer) => { body += c.toString(); });
155
+ req.on("data", (c: Buffer) => {
156
+ body += c.toString();
157
+ });
161
158
  req.on("end", () => {
162
159
  const p = new URLSearchParams(body);
163
160
  const choice = p.get("choice") ?? "";
164
161
  const freeText = p.get("freeText") ?? "";
165
- const final = choice === "__free__" || choice === "Redo" ? freeText.trim() : choice;
166
- res.writeHead(200, { "Content-Type": "text/html; charset=utf-8" });
167
- 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>");
162
+ const final =
163
+ choice === "__free__" || choice === "Redo"
164
+ ? freeText.trim()
165
+ : choice;
166
+ res.writeHead(200, {
167
+ "Content-Type": "text/html; charset=utf-8",
168
+ });
169
+ res.end(
170
+ `<!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>`,
171
+ );
168
172
  clearTimeout(timer);
169
173
  server.close();
170
174
  resolve(final || null);
@@ -172,25 +176,48 @@ document.addEventListener('keydown',e=>{if((e.ctrlKey||e.metaKey)&&e.key==='Ente
172
176
  }
173
177
  });
174
178
  server.listen(port);
175
- const { spawnSync } = require("node:child_process");
176
179
  const url = `http://localhost:${port}`;
177
- if (process.platform === "win32") spawnSync("cmd", ["/c", "start", "", url], { shell: false });
180
+ if (process.platform === "win32")
181
+ spawnSync("cmd", ["/c", "start", "", url], { shell: false });
178
182
  else if (process.platform === "darwin") spawnSync("open", [url]);
179
183
  else spawnSync("xdg-open", [url]);
180
- const timer = setTimeout(() => { server.close(); resolve(null); }, timeoutSeconds * 1000);
184
+ const timer = setTimeout(() => {
185
+ server.close();
186
+ resolve(null);
187
+ }, timeoutSeconds * 1000);
181
188
  });
182
189
  });
183
190
  };
184
191
 
185
- interviewHandler = (question, options, timeoutSeconds, plan, diff, confirmationMode) =>
186
- openBrowserInterview(interviewHTML(question, options, timeoutSeconds, plan, diff, confirmationMode), timeoutSeconds);
192
+ interviewHandler = (
193
+ question,
194
+ options,
195
+ timeoutSeconds,
196
+ plan,
197
+ diff,
198
+ confirmationMode,
199
+ ) =>
200
+ openBrowserInterview(
201
+ interviewHTML(
202
+ question,
203
+ options,
204
+ timeoutSeconds,
205
+ plan,
206
+ diff,
207
+ confirmationMode,
208
+ ),
209
+ timeoutSeconds,
210
+ );
187
211
 
188
212
  pi.registerTool({
189
213
  name: "interviewer",
190
214
  label: "Interview",
191
- 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.",
215
+ description:
216
+ "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.",
192
217
  parameters: Type.Object({
193
- question: Type.String({ description: "The question to present to the user" }),
218
+ question: Type.String({
219
+ description: "The question to present to the user",
220
+ }),
194
221
  options: Type.Optional(
195
222
  Type.Array(
196
223
  Type.Object({
@@ -208,14 +235,34 @@ document.addEventListener('keydown',e=>{if((e.ctrlKey||e.metaKey)&&e.key==='Ente
208
235
  }),
209
236
  ),
210
237
  ),
211
- plan: Type.Optional(Type.String({ description: "Refactoring plan (markdown) — shows in confirmation mode" })),
212
- diff: Type.Optional(Type.String({ description: "Unified diff text — shows in confirmation mode" })),
213
- confirmationMode: Type.Optional(Type.Boolean({ description: "Show plan+diff confirmation screen" })),
214
- timeoutSeconds: Type.Optional(Type.Number({ description: "Auto-close after this many seconds (default 600)" })),
238
+ plan: Type.Optional(
239
+ Type.String({
240
+ description:
241
+ "Refactoring plan (markdown) shows in confirmation mode",
242
+ }),
243
+ ),
244
+ diff: Type.Optional(
245
+ Type.String({
246
+ description: "Unified diff text — shows in confirmation mode",
247
+ }),
248
+ ),
249
+ confirmationMode: Type.Optional(
250
+ Type.Boolean({ description: "Show plan+diff confirmation screen" }),
251
+ ),
252
+ timeoutSeconds: Type.Optional(
253
+ Type.Number({
254
+ description: "Auto-close after this many seconds (default 600)",
255
+ }),
256
+ ),
215
257
  }),
216
258
  async execute(_toolCallId, input, _signal, _onUpdate, _ctx) {
217
259
  if (!interviewHandler)
218
- return { content: [{ type: "text" as const, text: "Interview tool not initialized" }], details: null };
260
+ return {
261
+ content: [
262
+ { type: "text" as const, text: "Interview tool not initialized" },
263
+ ],
264
+ details: null,
265
+ };
219
266
  const result = await interviewHandler(
220
267
  input.question,
221
268
  input.options ?? [],
@@ -224,7 +271,15 @@ document.addEventListener('keydown',e=>{if((e.ctrlKey||e.metaKey)&&e.key==='Ente
224
271
  input.diff,
225
272
  input.confirmationMode,
226
273
  );
227
- return { content: [{ type: "text" as const, text: result ?? "No response (timed out or dismissed)" }], details: result ?? null };
274
+ return {
275
+ content: [
276
+ {
277
+ type: "text" as const,
278
+ text: result ?? "No response (timed out or dismissed)",
279
+ },
280
+ ],
281
+ details: result ?? null,
282
+ };
228
283
  },
229
284
  });
230
285
 
@@ -38,7 +38,7 @@ export class KnipClient {
38
38
  /**
39
39
  * Run knip analysis on the project
40
40
  */
41
- analyze(cwd) {
41
+ analyze(cwd, ignore) {
42
42
  if (!this.isAvailable()) {
43
43
  return {
44
44
  success: false,
@@ -52,12 +52,16 @@ export class KnipClient {
52
52
  }
53
53
  const targetDir = cwd || process.cwd();
54
54
  try {
55
- const result = spawnSync("npx", [
55
+ const args = [
56
56
  "knip",
57
57
  "--reporter=json",
58
58
  "--include",
59
- "exports,types,dependencies,unlisted",
60
- ], {
59
+ "files,exports,types,dependencies,unlisted",
60
+ ];
61
+ if (ignore && ignore.length > 0) {
62
+ args.push("--ignore", ignore.join(","));
63
+ }
64
+ const result = spawnSync("npx", args, {
61
65
  encoding: "utf-8",
62
66
  timeout: 30000,
63
67
  cwd: targetDir,
@@ -65,6 +69,10 @@ export class KnipClient {
65
69
  });
66
70
  // Knip exits 0 on success (even with issues), 1 on errors
67
71
  const output = result.stdout || "";
72
+ this.log(`Knip output length: ${output.length}`);
73
+ if (output.length < 500) {
74
+ this.log(`Knip output sample: ${output}`);
75
+ }
68
76
  if (!output.trim()) {
69
77
  return {
70
78
  success: true,
@@ -67,7 +67,7 @@ export class KnipClient {
67
67
  /**
68
68
  * Run knip analysis on the project
69
69
  */
70
- analyze(cwd?: string): KnipResult {
70
+ analyze(cwd?: string, ignore?: string[]): KnipResult {
71
71
  if (!this.isAvailable()) {
72
72
  return {
73
73
  success: false,
@@ -83,24 +83,29 @@ export class KnipClient {
83
83
  const targetDir = cwd || process.cwd();
84
84
 
85
85
  try {
86
- const result = spawnSync(
87
- "npx",
88
- [
89
- "knip",
90
- "--reporter=json",
91
- "--include",
92
- "exports,types,dependencies,unlisted",
93
- ],
94
- {
95
- encoding: "utf-8",
96
- timeout: 30000,
97
- cwd: targetDir,
98
- shell: true,
99
- },
100
- );
86
+ const args = [
87
+ "knip",
88
+ "--reporter=json",
89
+ "--include",
90
+ "files,exports,types,dependencies,unlisted",
91
+ ];
92
+ if (ignore && ignore.length > 0) {
93
+ args.push("--ignore", ignore.join(","));
94
+ }
95
+
96
+ const result = spawnSync("npx", args, {
97
+ encoding: "utf-8",
98
+ timeout: 30000,
99
+ cwd: targetDir,
100
+ shell: true,
101
+ });
101
102
 
102
103
  // Knip exits 0 on success (even with issues), 1 on errors
103
104
  const output = result.stdout || "";
105
+ this.log(`Knip output length: ${output.length}`);
106
+ if (output.length < 500) {
107
+ this.log(`Knip output sample: ${output}`);
108
+ }
104
109
  if (!output.trim()) {
105
110
  return {
106
111
  success: true,