@ikyyofc/gemini-cli 3.0.0 → 3.0.1

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 (2) hide show
  1. package/package.json +1 -1
  2. package/src/renderer.js +205 -88
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@ikyyofc/gemini-cli",
3
- "version": "3.0.0",
3
+ "version": "3.0.1",
4
4
  "description": "AI Agent CLI — native function calling · GEMINI.md context · extensions",
5
5
  "type": "module",
6
6
  "bin": { "gemini": "./index.js" },
package/src/renderer.js CHANGED
@@ -13,16 +13,18 @@ const tw = () => Math.min(process.stdout.columns || 72, 84);
13
13
  const bw = () => Math.min(tw() - 4, 68);
14
14
  const vlen = s => s.replace(/\x1b\[[0-9;]*m/g, "").length;
15
15
 
16
- function wrapText(text, maxW) {
17
- if (vlen(text) <= maxW) return [text];
16
+ // Word-wrap a PLAIN (no ANSI) string to maxW, returns array of lines
17
+ function wrapPlain(text, maxW) {
18
+ if (!text || text.length <= maxW) return [text || ""];
18
19
  const words = text.split(" ");
19
20
  const lines = [];
20
21
  let cur = "";
21
22
  for (const w of words) {
22
- const adding = cur ? cur + " " + w : w;
23
- if (vlen(adding) <= maxW) { cur = adding; continue; }
23
+ const candidate = cur ? cur + " " + w : w;
24
+ if (candidate.length <= maxW) { cur = candidate; continue; }
24
25
  if (cur) lines.push(cur);
25
- if (vlen(w) > maxW) {
26
+ if (w.length > maxW) {
27
+ // hard-wrap single very long word (e.g. bare URL)
26
28
  for (let i = 0; i < w.length; i += maxW) lines.push(w.slice(i, i + maxW));
27
29
  cur = "";
28
30
  } else { cur = w; }
@@ -31,37 +33,34 @@ function wrapText(text, maxW) {
31
33
  return lines.length ? lines : [""];
32
34
  }
33
35
 
34
- // ─── Syntax highlight ────────────────────────────────────────────
35
- // FIX: applyToRaw() ensures the number regex never matches digits
36
- // inside existing \x00N\x00 placeholder markers (the root cause of
37
- // garbage numbers appearing in rendered code).
36
+ // ─────────────────────────────────────────────────────────────────
37
+ // Syntax highlighting applyToRaw prevents placeholder corruption
38
+ // ─────────────────────────────────────────────────────────────────
38
39
  const KW = {
39
- js: /\b(const|let|var|function|return|if|else|for|while|do|switch|case|break|continue|new|this|class|extends|import|export|default|async|await|try|catch|finally|throw|typeof|instanceof|of|in|null|undefined|true|false|void|delete|yield|from|static|super)\b/g,
40
- ts: /\b(const|let|var|function|return|if|else|for|while|switch|case|class|extends|import|export|default|async|await|try|catch|type|interface|enum|implements|declare|readonly|abstract|as|keyof|never|any|string|number|boolean|null|undefined|true|false)\b/g,
41
- py: /\b(def|class|return|if|elif|else|for|while|import|from|as|with|try|except|finally|raise|pass|break|continue|and|or|not|in|is|None|True|False|lambda|yield|global|async|await)\b/g,
42
- go: /\b(func|return|if|else|for|range|switch|var|const|type|struct|interface|import|package|defer|go|chan|map|make|new|nil|true|false)\b/g,
43
- sh: /\b(if|then|else|elif|fi|for|while|do|done|case|esac|function|return|exit|export|echo|local|source|cd|mkdir|rm|cp|mv|sudo|npm|pip|git)\b/g,
44
- rs: /\b(fn|let|mut|return|if|else|for|match|use|mod|pub|struct|enum|impl|trait|type|const|async|await|true|false|None|Some|Ok|Err)\b/g,
40
+ js:/\b(const|let|var|function|return|if|else|for|while|do|switch|case|break|continue|new|this|class|extends|import|export|default|async|await|try|catch|finally|throw|typeof|instanceof|of|in|null|undefined|true|false|void|delete|yield|from|static|super)\b/g,
41
+ ts:/\b(const|let|var|function|return|if|else|for|while|switch|case|class|extends|import|export|default|async|await|try|catch|type|interface|enum|implements|declare|readonly|abstract|as|keyof|never|any|string|number|boolean|null|undefined|true|false)\b/g,
42
+ py:/\b(def|class|return|if|elif|else|for|while|import|from|as|with|try|except|finally|raise|pass|break|continue|and|or|not|in|is|None|True|False|lambda|yield|global|async|await)\b/g,
43
+ go:/\b(func|return|if|else|for|range|switch|var|const|type|struct|interface|import|package|defer|go|chan|map|make|new|nil|true|false)\b/g,
44
+ sh:/\b(if|then|else|elif|fi|for|while|do|done|case|esac|function|return|exit|export|echo|local|source|cd|mkdir|rm|cp|mv|sudo|npm|pip|git)\b/g,
45
+ rs:/\b(fn|let|mut|return|if|else|for|match|use|mod|pub|struct|enum|impl|trait|type|const|async|await|true|false|None|Some|Ok|Err)\b/g,
45
46
  };
46
47
  const LANGMAP = {
47
- javascript:"js", js:"js", typescript:"ts", ts:"ts",
48
- python:"py", py:"py", go:"go", golang:"go",
49
- rust:"rs", rs:"rs", bash:"sh", sh:"sh", shell:"sh", zsh:"sh", fish:"sh",
48
+ javascript:"js",js:"js",typescript:"ts",ts:"ts",
49
+ python:"py",py:"py",go:"go",golang:"go",
50
+ rust:"rs",rs:"rs",bash:"sh",sh:"sh",shell:"sh",zsh:"sh",fish:"sh",
50
51
  };
51
52
 
52
53
  function applyToRaw(str, re, fn) {
53
- return str.split(/(\x00\d+\x00)/).map((p, i) =>
54
- i % 2 === 0 ? p.replace(re, fn) : p
55
- ).join("");
54
+ return str.split(/(\x00\d+\x00)/).map((p, i) => i % 2 === 0 ? p.replace(re, fn) : p).join("");
56
55
  }
57
56
 
58
57
  function highlight(code, lang = "") {
59
58
  const l = LANGMAP[lang.toLowerCase()] || "";
60
59
  if (!l) return code;
61
60
  const saved = [];
62
- const save = s => { const id = `\x00${saved.length}\x00`; saved.push(s); return id; };
61
+ const save = s => { const id = `\x00${saved.length}\x00`; saved.push(s); return id; };
63
62
  let r = code;
64
- r = applyToRaw(r, /(\/\/.*$|#.*$|\/\*[\s\S]*?\*\/)/gm, m => save(chalk.hex(C.comment).italic(m)));
63
+ r = applyToRaw(r, /(\/\/.*$|#.*$|\/\*[\s\S]*?\*\/)/gm, m => save(chalk.hex(C.comment).italic(m)));
65
64
  r = applyToRaw(r, /("(?:[^"\\]|\\.)*"|'(?:[^'\\]|\\.)*'|`(?:[^`\\]|\\.)*`)/g, m => save(chalk.hex(C.str)(m)));
66
65
  if (KW[l]) { KW[l].lastIndex = 0; r = applyToRaw(r, KW[l], m => save(chalk.hex(C.kw).bold(m))); }
67
66
  r = applyToRaw(r, /\b(\d+\.?\d*)\b/g, m => save(chalk.hex(C.num)(m)));
@@ -69,53 +68,162 @@ function highlight(code, lang = "") {
69
68
  return r.replace(/\x00(\d+)\x00/g, (_, i) => saved[+i] ?? "");
70
69
  }
71
70
 
72
- // ─── Markdown renderer ───────────────────────────────────────────
71
+ // ─────────────────────────────────────────────────────────────────
72
+ // Markdown → array of display lines (with wrapping baked in)
73
+ // Returns: Array<{ text: string, raw: boolean }>
74
+ // raw=true → ANSI already applied (code blocks), print as-is
75
+ // raw=false → plain text, will receive border + chalk in printBox
76
+ // ─────────────────────────────────────────────────────────────────
73
77
  export function renderMarkdown(text, contentW) {
74
78
  const cw = contentW ?? bw() - 2;
75
- let r = text;
76
-
77
- r = r.replace(/```(\w*)\n?([\s\S]*?)```/g, (_, lang, code) => {
78
- const trimmed = code.trimEnd();
79
- const lines = trimmed.split("\n");
80
- const hl = highlight(trimmed, lang);
81
- const hlLines = hl.split("\n");
82
- const gw = String(lines.length).length;
83
- const bw2 = Math.min(cw + 2, tw() - 6);
84
- const lbl = lang ? chalk.hex(C.blue).bold(` ${lang} `) : "";
85
- const lblLen = lang ? lang.length + 2 : 0;
86
- const dashes = Math.max(2, bw2 - lblLen - 1);
87
- const maxCode = bw2 - gw - 4;
88
-
89
- const top = chalk.hex(C.dim)("┌─") + lbl + chalk.hex(C.dimmer)("─".repeat(dashes)) + chalk.hex(C.dim)("┐");
90
- const bot = chalk.hex(C.dim)("└" + "─".repeat(bw2 + 1) + "┘");
91
- const body = hlLines.map((hl_line, i) => {
92
- const ln = chalk.hex(C.dimmer)(String(i + 1).padStart(gw));
93
- const raw = lines[i] ?? "";
94
- const disp = raw.length > maxCode ? hl_line.slice(0, maxCode * 3) + chalk.hex(C.dimmer)("…") : hl_line;
95
- return chalk.hex(C.dim)("│") + chalk.hex(C.dimmer)(" " + ln + " ╎ ") + disp;
96
- }).join("\n");
97
-
98
- return `\n${top}\n${body}\n${bot}\n`;
99
- });
100
-
101
- r = r.replace(/`([^`\n]+)`/g, (_, c) => chalk.bgHex("#2A2A3E")(chalk.hex(C.orange)(" " + c + " ")));
102
- r = r.replace(/^### (.+)$/gm, (_, t) => "\n" + chalk.hex(C.yellow).bold("◈ " + t));
103
- r = r.replace(/^## (.+)$/gm, (_, t) => "\n" + chalk.hex(C.blue).bold.underline(t));
104
- r = r.replace(/^# (.+)$/gm, (_, t) => "\n" + chalk.hex(C.teal).bold("" + t.toUpperCase()));
105
- r = r.replace(/\*\*\*(.+?)\*\*\*/g, (_, t) => chalk.bold.italic(t));
106
- r = r.replace(/\*\*(.+?)\*\*/g, (_, t) => chalk.bold(t));
107
- r = r.replace(/\*(.+?)\*/g, (_, t) => chalk.italic(t));
108
- r = r.replace(/^> (.+)$/gm, (_, t) => chalk.hex(C.purple)("") + chalk.hex(C.muted).italic(t));
109
- r = r.replace(/^(\s*)[*\-+] (.+)$/gm, (_, i, t) => i + chalk.hex(C.teal)("") + chalk.hex(C.white)(t));
110
- r = r.replace(/^(\s*)(\d+)\. (.+)$/gm, (_, i, n, t) => i + chalk.hex(C.blue)(chalk.bold(n + ".") + " ") + chalk.hex(C.white)(t));
111
- r = r.replace(/^---+$/gm, chalk.hex(C.dimmer)("".repeat(Math.min(cw, 48))));
112
- return r;
79
+ const out = []; // { text, raw }
80
+
81
+ const pushPlain = (s) => out.push({ text: s, raw: false });
82
+ const pushStyled = (s) => out.push({ text: s, raw: true });
83
+ const pushBlank = () => out.push({ text: "", raw: false });
84
+
85
+ // Normalise line endings
86
+ const src = text.replace(/\r\n/g, "\n").trimEnd();
87
+
88
+ // Split into tokens: code blocks vs everything else
89
+ const segments = src.split(/(```[\s\S]*?```)/g);
90
+
91
+ for (const seg of segments) {
92
+ // ── Fenced code block ──────────────────────────────────────
93
+ if (seg.startsWith("```")) {
94
+ const m = seg.match(/^```(\w*)\n?([\s\S]*?)```$/);
95
+ const lang = m?.[1] ?? "";
96
+ const code = m?.[2]?.trimEnd() ?? seg.slice(3);
97
+ const lines = code.split("\n");
98
+ const hl = highlight(code, lang);
99
+ const hlLines = hl.split("\n");
100
+ const gw = String(lines.length).length;
101
+ const bw2 = Math.min(cw + 2, tw() - 6);
102
+ const lbl = lang ? chalk.hex(C.blue).bold(` ${lang} `) : "";
103
+ const lblLen = lang ? lang.length + 2 : 0;
104
+ const dashes = Math.max(2, bw2 - lblLen - 1);
105
+ const maxCode = bw2 - gw - 4;
106
+
107
+ pushBlank();
108
+ pushStyled(chalk.hex(C.dim)("┌─") + lbl + chalk.hex(C.dimmer)("".repeat(dashes)) + chalk.hex(C.dim)("┐"));
109
+ hlLines.forEach((hl_line, i) => {
110
+ const ln = chalk.hex(C.dimmer)(String(i + 1).padStart(gw));
111
+ const raw = lines[i] ?? "";
112
+ const disp = raw.length > maxCode ? hl_line.slice(0, maxCode * 3) + chalk.hex(C.dimmer)("") : hl_line;
113
+ pushStyled(chalk.hex(C.dim)("") + chalk.hex(C.dimmer)(" " + ln + " ╎ ") + disp);
114
+ });
115
+ pushStyled(chalk.hex(C.dim)("" + "─".repeat(bw2 + 1) + "┘"));
116
+ pushBlank();
117
+ continue;
118
+ }
119
+
120
+ // ── Text segment — process line by line ───────────────────
121
+ for (const rawLine of seg.split("\n")) {
122
+ const line = rawLine;
123
+
124
+ // Blank line
125
+ if (!line.trim()) { pushBlank(); continue; }
126
+
127
+ // === Header === (non-standard but model uses it)
128
+ if (/^={3,}\s*.+\s*={3,}$/.test(line)) {
129
+ const t = line.replace(/^=+\s*/, "").replace(/\s*=+$/, "");
130
+ pushBlank();
131
+ pushStyled(chalk.hex(C.blue).bold.underline(t));
132
+ pushBlank();
133
+ continue;
134
+ }
135
+
136
+ // ATX headers
137
+ if (/^### /.test(line)) { pushStyled(chalk.hex(C.yellow).bold("◈ " + line.slice(4))); continue; }
138
+ if (/^## /.test(line)) { pushStyled(chalk.hex(C.blue).bold.underline(line.slice(3))); continue; }
139
+ if (/^# /.test(line)) { pushStyled(chalk.hex(C.teal).bold("◉ " + line.slice(2).toUpperCase())); continue; }
140
+
141
+ // HR
142
+ if (/^---+$/.test(line)) { pushStyled(chalk.hex(C.dimmer)("╌".repeat(Math.min(cw, 48)))); continue; }
143
+
144
+ // Blockquote
145
+ if (/^> /.test(line)) {
146
+ const t = line.slice(2);
147
+ wrapPlain(t, cw - 2).forEach((wl, i) =>
148
+ pushStyled(chalk.hex(C.purple)("▎ ") + chalk.hex(C.muted).italic(wl))
149
+ );
150
+ continue;
151
+ }
152
+
153
+ // Unordered list
154
+ const ulM = line.match(/^(\s*)[*\-+] (.+)$/);
155
+ if (ulM) {
156
+ const [, ind, content] = ulM;
157
+ const indW = ind.length;
158
+ const itemW = cw - indW - 4; // "◆ " = 2 chars visible + 2 padding
159
+ const plain = stripInline(content);
160
+ wrapPlain(plain, itemW).forEach((wl, i) => {
161
+ if (i === 0)
162
+ pushStyled(ind + chalk.hex(C.teal)("◆ ") + chalk.hex(C.white)(applyInline(wl)));
163
+ else
164
+ pushStyled(ind + " " + chalk.hex(C.white)(applyInline(wl)));
165
+ });
166
+ continue;
167
+ }
168
+
169
+ // Ordered list
170
+ const olM = line.match(/^(\s*)(\d+)\. (.+)$/);
171
+ if (olM) {
172
+ const [, ind, num, content] = olM;
173
+ const markerW = num.length + 2;
174
+ const itemW = cw - ind.length - markerW;
175
+ const plain = stripInline(content);
176
+ wrapPlain(plain, itemW).forEach((wl, i) => {
177
+ if (i === 0)
178
+ pushStyled(ind + chalk.hex(C.blue)(chalk.bold(num + ".") + " ") + chalk.hex(C.white)(applyInline(wl)));
179
+ else
180
+ pushStyled(ind + " ".repeat(markerW) + chalk.hex(C.white)(applyInline(wl)));
181
+ });
182
+ continue;
183
+ }
184
+
185
+ // Plain paragraph — strip inline markup for wrapping, then re-apply
186
+ const plain = stripInline(line);
187
+ wrapPlain(plain, cw).forEach(wl =>
188
+ pushStyled(chalk.hex(C.white)(applyInline(wl)))
189
+ );
190
+ }
191
+ }
192
+
193
+ return out;
194
+ }
195
+
196
+ // Strip inline markdown to get plain text for accurate wrapping
197
+ function stripInline(s) {
198
+ return s
199
+ .replace(/\[([^\]]+)\]\([^)]+\)/g, "$1") // links → text only
200
+ .replace(/`([^`]+)`/g, "$1")
201
+ .replace(/\*\*\*(.+?)\*\*\*/g, "$1")
202
+ .replace(/\*\*(.+?)\*\*/g, "$1")
203
+ .replace(/\*(.+?)\*/g, "$1")
204
+ .replace(/_(.+?)_/g, "$1");
205
+ }
206
+
207
+ // Apply inline markdown styling to already-wrapped line
208
+ function applyInline(s) {
209
+ return s
210
+ // Links → show "text (url truncated)" — URLs not clickable in terminal
211
+ .replace(/\[([^\]]+)\]\(([^)]+)\)/g, (_, text, url) => {
212
+ const short = url.length > 40 ? url.slice(0, 40) + "…" : url;
213
+ return chalk.hex(C.teal)(text) + chalk.hex(C.dimmer)(" (" + short + ")");
214
+ })
215
+ .replace(/`([^`]+)`/g, (_, c) => chalk.bgHex("#2A2A3E")(chalk.hex(C.orange)(" " + c + " ")))
216
+ .replace(/\*\*\*(.+?)\*\*\*/g, (_, t) => chalk.bold.italic(t))
217
+ .replace(/\*\*(.+?)\*\*/g, (_, t) => chalk.bold(t))
218
+ .replace(/\*(.+?)\*/g, (_, t) => chalk.italic(t))
219
+ .replace(/_(.+?)_/g, (_, t) => chalk.italic(t));
113
220
  }
114
221
 
115
- // ─── Box printer ─────────────────────────────────────────────────
116
- function printBox(contentLines, borderHex, label, extra = "") {
222
+ // ─────────────────────────────────────────────────────────────────
223
+ // Box printer all lines are pre-wrapped, just add border
224
+ // ─────────────────────────────────────────────────────────────────
225
+ function printBox(renderedLines, borderHex, label, extra = "") {
117
226
  const W = bw();
118
- const inner = W - 2;
119
227
  const lblLen = vlen(label) + vlen(extra);
120
228
  const dashes = Math.max(0, W - lblLen - 1);
121
229
 
@@ -124,37 +232,43 @@ function printBox(contentLines, borderHex, label, extra = "") {
124
232
  chalk.hex(C.dim)("─".repeat(dashes)) + extra + "\n"
125
233
  );
126
234
 
127
- for (const line of contentLines) {
128
- if (line.includes("\x1b[")) {
129
- // Already styled (code blocks, headers) print as-is
130
- process.stdout.write(chalk.hex(borderHex)("│ ") + line + "\n");
131
- } else {
132
- wrapText(line || " ", inner).forEach(wl =>
133
- process.stdout.write(chalk.hex(borderHex)("│ ") + wl + "\n")
134
- );
135
- }
235
+ for (const { text, raw } of renderedLines) {
236
+ // raw lines (code blocks, headers with box-drawing) — print as-is
237
+ // non-raw lines — already plain/styled text, just add border
238
+ process.stdout.write(chalk.hex(borderHex)("│ ") + text + "\n");
136
239
  }
137
240
 
138
241
  process.stdout.write(chalk.hex(borderHex)("╰" + "─".repeat(W + 1)) + "\n");
139
242
  }
140
243
 
141
- // ─── Messages ────────────────────────────────────────────────────
244
+ // ─────────────────────────────────────────────────────────────────
245
+ // Messages
246
+ // ─────────────────────────────────────────────────────────────────
142
247
  export function printUser(text) {
143
- const lines = text.split("\n");
144
- const multi = lines.length > 1;
145
- const label = chalk.hex(C.blue).bold(" you ");
146
- const extra = multi ? chalk.hex(C.muted)(` (${lines.length} lines) `) : "";
248
+ const W = bw();
249
+ const inner = W - 2;
250
+ const rawLines = text.split("\n");
251
+ const multi = rawLines.length > 1;
252
+ const label = chalk.hex(C.blue).bold(" you ");
253
+ const extra = multi ? chalk.hex(C.muted)(` (${rawLines.length} lines) `) : "";
254
+
255
+ const lines = multi
256
+ ? rawLines.flatMap(l => wrapPlain(l, inner).map(wl => ({ text: chalk.hex(C.white)(wl), raw: true })))
257
+ : wrapPlain(text, inner).map(wl => ({ text: chalk.hex(C.white)(wl), raw: true }));
258
+
147
259
  printBox(lines, C.blue, label, extra);
148
260
  }
149
261
 
150
262
  export function printAssistant(text) {
151
- const inner = bw() - 2;
152
- const rendered = renderMarkdown(text.trimEnd(), inner);
153
- printBox(rendered.split("\n"), C.teal, chalk.hex(C.teal).bold(" gemini "));
263
+ const inner = bw() - 2;
264
+ const lines = renderMarkdown(text.trimEnd(), inner);
265
+ printBox(lines, C.teal, chalk.hex(C.teal).bold(" gemini "));
154
266
  process.stdout.write("\n");
155
267
  }
156
268
 
157
- // ─── Agent step blocks ───────────────────────────────────────────
269
+ // ─────────────────────────────────────────────────────────────────
270
+ // Agent step blocks
271
+ // ─────────────────────────────────────────────────────────────────
158
272
  export function printStepHeader(step) {
159
273
  const W = bw();
160
274
  const label = chalk.hex(C.yellow).bold(" working ");
@@ -173,7 +287,7 @@ export function printStepFooter() {
173
287
  export function printToolCall(name, args = {}) {
174
288
  const argStr = Object.entries(args).map(([k, v]) => {
175
289
  const raw = String(v).replace(/\n/g, "↵");
176
- const val = vlen(raw) > 42 ? raw.slice(0, 42) + "…" : raw;
290
+ const val = raw.length > 42 ? raw.slice(0, 42) + "…" : raw;
177
291
  return chalk.hex(C.muted)(k + ":") + chalk.hex(C.orange)(val);
178
292
  }).join(" ");
179
293
  process.stdout.write(
@@ -199,15 +313,19 @@ export function printToolResult(result) {
199
313
  if (extra > 0) process.stdout.write(border + chalk.hex(C.dim)(`… +${extra} more lines`) + "\n");
200
314
  }
201
315
 
202
- // ─── Status ──────────────────────────────────────────────────────
316
+ // ─────────────────────────────────────────────────────────────────
317
+ // Status
318
+ // ─────────────────────────────────────────────────────────────────
203
319
  export function printError(msg) { process.stdout.write("\n" + chalk.hex(C.red)("╳ ") + chalk.hex(C.red).bold("error ") + chalk.hex(C.muted)(msg) + "\n\n"); }
204
320
  export function printInfo(msg) { process.stdout.write(chalk.hex(C.dim)("· ") + chalk.hex(C.white)(msg) + "\n"); }
205
321
  export function printSuccess(msg) { process.stdout.write(chalk.hex(C.teal)("✓ ") + chalk.hex(C.teal)(msg) + "\n"); }
206
322
  export function printWarning(msg) { process.stdout.write(chalk.hex(C.yellow)("⚠ ") + chalk.hex(C.yellow)(msg) + "\n"); }
207
323
 
208
- // ─── Welcome ─────────────────────────────────────────────────────
324
+ // ─────────────────────────────────────────────────────────────────
325
+ // Welcome & Help
326
+ // ─────────────────────────────────────────────────────────────────
209
327
  export function renderWelcome(memCount = 0, extCount = 0) {
210
- const W = bw();
328
+ const W = bw();
211
329
  const stats = [
212
330
  memCount ? `${memCount} context file${memCount > 1 ? "s" : ""}` : null,
213
331
  extCount ? `${extCount} extension${extCount > 1 ? "s" : ""}` : null,
@@ -224,7 +342,6 @@ export function renderWelcome(memCount = 0, extCount = 0) {
224
342
  ].join("\n");
225
343
  }
226
344
 
227
- // ─── Help ────────────────────────────────────────────────────────
228
345
  export function renderHelp(customCommands = {}) {
229
346
  const sep = chalk.hex(C.dimmer)(" " + "─".repeat(Math.min(tw() - 4, 52)));
230
347
  const row = (cmd, desc) => " " + chalk.hex(C.blue).bold(cmd.padEnd(24)) + chalk.hex(C.muted)(desc);