@compilr-dev/cli 0.6.5 → 0.7.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 (31) hide show
  1. package/dist/commands-v2/handlers/core.js +10 -1
  2. package/dist/commands-v2/handlers/index.d.ts +1 -0
  3. package/dist/commands-v2/handlers/index.js +3 -0
  4. package/dist/commands-v2/handlers/settings.js +21 -5
  5. package/dist/commands-v2/handlers/skill.d.ts +10 -0
  6. package/dist/commands-v2/handlers/skill.js +63 -0
  7. package/dist/commands-v2/index.d.ts +1 -1
  8. package/dist/commands-v2/index.js +1 -1
  9. package/dist/commands-v2/registry.d.ts +4 -0
  10. package/dist/commands-v2/registry.js +19 -0
  11. package/dist/compilr-diff-companion.vsix +0 -0
  12. package/dist/index.js +8 -12
  13. package/dist/repl-helpers.d.ts +29 -1
  14. package/dist/repl-helpers.js +77 -7
  15. package/dist/repl-v2.js +29 -3
  16. package/dist/ui/conversation.js +1 -2
  17. package/dist/ui/markdown-renderer.d.ts +43 -0
  18. package/dist/ui/markdown-renderer.js +474 -0
  19. package/dist/ui/overlay/impl/artifact-detail-overlay-v2.js +1 -2
  20. package/dist/ui/overlay/impl/document-detail-overlay-v2.js +1 -2
  21. package/dist/ui/overlay/impl/help-overlay-v2.d.ts +7 -1
  22. package/dist/ui/overlay/impl/help-overlay-v2.js +19 -2
  23. package/dist/ui/overlay/impl/skill-detail-overlay-v2.d.ts +91 -0
  24. package/dist/ui/overlay/impl/skill-detail-overlay-v2.js +863 -0
  25. package/dist/ui/overlay/impl/skill-editor-overlay.d.ts +56 -0
  26. package/dist/ui/overlay/impl/skill-editor-overlay.js +493 -0
  27. package/dist/ui/overlay/impl/skills-overlay-v2.d.ts +83 -0
  28. package/dist/ui/overlay/impl/skills-overlay-v2.js +1095 -0
  29. package/dist/utils/skill-paths.d.ts +21 -0
  30. package/dist/utils/skill-paths.js +44 -0
  31. package/package.json +7 -6
@@ -0,0 +1,474 @@
1
+ /**
2
+ * Terminal markdown renderer for marked.
3
+ * Drop-in replacement for `marked-terminal` — returns a marked extension.
4
+ *
5
+ * Usage:
6
+ * import { markedTerminal } from './markdown-renderer.js';
7
+ * marked.use(markedTerminal(options));
8
+ */
9
+ import { Chalk } from 'chalk';
10
+ import { highlight as cliHighlight } from 'cli-highlight';
11
+ import Table from 'cli-table3';
12
+ import ansiEscapes from 'ansi-escapes';
13
+ import supportsHyperlinks from 'supports-hyperlinks';
14
+ // ---------------------------------------------------------------------------
15
+ // Constants
16
+ // ---------------------------------------------------------------------------
17
+ const TABLE_CELL_SPLIT = '^*||*^';
18
+ const TABLE_ROW_WRAP = '*|*|*|*';
19
+ const TABLE_ROW_WRAP_RE = new RegExp(TABLE_ROW_WRAP.replace(/[.*+?^${}()|[\]\\]/g, '\\$&'), 'g');
20
+ const COLON_REPLACER = '*#COLON|*';
21
+ const COLON_REPLACER_RE = new RegExp(COLON_REPLACER.replace(/[.*+?^${}()|[\]\\]/g, '\\$&'), 'g');
22
+ const HARD_RETURN = '\r';
23
+ const HARD_RETURN_RE = new RegExp(HARD_RETURN);
24
+ const HARD_RETURN_GFM_RE = new RegExp(HARD_RETURN + '|<br />');
25
+ const BULLET_POINT = '* ';
26
+ // eslint-disable-next-line no-control-regex
27
+ const ANSI_RE = /\u001b\[(?:\d{1,3})(?:;\d{1,3})*m/g;
28
+ // ---------------------------------------------------------------------------
29
+ // Defaults
30
+ // ---------------------------------------------------------------------------
31
+ const chalk = new Chalk({ level: 3 });
32
+ const defaultOptions = {
33
+ heading: chalk.green.bold,
34
+ firstHeading: chalk.magenta.underline.bold,
35
+ strong: chalk.bold,
36
+ em: chalk.italic,
37
+ codespan: chalk.yellow,
38
+ code: chalk.yellow,
39
+ blockquote: chalk.gray.italic,
40
+ del: chalk.dim.gray.strikethrough,
41
+ link: chalk.blue,
42
+ href: chalk.blue.underline,
43
+ html: chalk.gray,
44
+ hr: chalk.reset,
45
+ table: chalk.reset,
46
+ listitem: chalk.reset,
47
+ paragraph: chalk.reset,
48
+ text: (s) => s,
49
+ width: 80,
50
+ tab: 2,
51
+ reflowText: true,
52
+ showSectionPrefix: false,
53
+ unescape: true,
54
+ emoji: false,
55
+ tableOptions: {},
56
+ };
57
+ // ---------------------------------------------------------------------------
58
+ // Utilities
59
+ // ---------------------------------------------------------------------------
60
+ function identity(s) { return s; }
61
+ function textLength(str) {
62
+ return str.replace(ANSI_RE, '').length;
63
+ }
64
+ function section(text) {
65
+ return text + '\n\n';
66
+ }
67
+ function indentify(indent, text) {
68
+ if (!text)
69
+ return text;
70
+ return indent + text.split('\n').join('\n' + indent);
71
+ }
72
+ function indentLines(indent, text) {
73
+ return text.replace(/(^|\n)(.+)/g, '$1' + indent + '$2');
74
+ }
75
+ function unescapeEntities(html) {
76
+ return html
77
+ .replace(/&amp;/g, '&')
78
+ .replace(/&lt;/g, '<')
79
+ .replace(/&gt;/g, '>')
80
+ .replace(/&quot;/g, '"')
81
+ .replace(/&#39;/g, "'");
82
+ }
83
+ function undoColon(str) {
84
+ return str.replace(COLON_REPLACER_RE, ':');
85
+ }
86
+ function fixHardReturn(text, reflow) {
87
+ return reflow ? text.replace(HARD_RETURN_RE, '\n') : text;
88
+ }
89
+ function hr(char, length) {
90
+ const width = length || process.stdout.columns || 80;
91
+ return char.repeat(width);
92
+ }
93
+ function highlightCode(code, language, fallbackStyle) {
94
+ if (chalk.level === 0)
95
+ return code;
96
+ try {
97
+ return cliHighlight(code, { language: language || 'plaintext' });
98
+ }
99
+ catch {
100
+ return fallbackStyle(code);
101
+ }
102
+ }
103
+ // ---------------------------------------------------------------------------
104
+ // Reflow
105
+ // ---------------------------------------------------------------------------
106
+ function reflowText(text, width, gfm) {
107
+ const splitRe = gfm ? HARD_RETURN_GFM_RE : HARD_RETURN_RE;
108
+ const sections = text.split(splitRe);
109
+ const reflowed = [];
110
+ for (const sec of sections) {
111
+ // eslint-disable-next-line no-control-regex
112
+ const fragments = sec.split(/(\u001b\[(?:\d{1,3})(?:;\d{1,3})*m)/g);
113
+ let column = 0;
114
+ let currentLine = '';
115
+ let lastWasEscape = false;
116
+ for (const fragment of fragments) {
117
+ if (fragment === '') {
118
+ lastWasEscape = false;
119
+ continue;
120
+ }
121
+ if (!textLength(fragment)) {
122
+ currentLine += fragment;
123
+ lastWasEscape = true;
124
+ continue;
125
+ }
126
+ const words = fragment.split(/[ \t\n]+/);
127
+ for (const word of words) {
128
+ if (!word)
129
+ continue;
130
+ const addSpace = column !== 0 && !lastWasEscape;
131
+ const neededWidth = word.length + (addSpace ? 1 : 0);
132
+ if (column + neededWidth > width && word.length <= width) {
133
+ reflowed.push(currentLine);
134
+ currentLine = word;
135
+ column = word.length;
136
+ }
137
+ else if (column + neededWidth > width) {
138
+ // Word longer than width — split it
139
+ let remaining = word;
140
+ const space = width - column - (addSpace ? 1 : 0);
141
+ if (space > 0) {
142
+ if (addSpace)
143
+ currentLine += ' ';
144
+ currentLine += remaining.substring(0, space);
145
+ reflowed.push(currentLine);
146
+ remaining = remaining.substring(space);
147
+ }
148
+ while (remaining.length > width) {
149
+ reflowed.push(remaining.substring(0, width));
150
+ remaining = remaining.substring(width);
151
+ }
152
+ currentLine = remaining;
153
+ column = remaining.length;
154
+ }
155
+ else {
156
+ if (addSpace) {
157
+ currentLine += ' ';
158
+ column++;
159
+ }
160
+ currentLine += word;
161
+ column += word.length;
162
+ }
163
+ lastWasEscape = false;
164
+ }
165
+ }
166
+ if (textLength(currentLine))
167
+ reflowed.push(currentLine);
168
+ }
169
+ return reflowed.join('\n');
170
+ }
171
+ // ---------------------------------------------------------------------------
172
+ // List helpers
173
+ // ---------------------------------------------------------------------------
174
+ function isPointedLine(line, indent) {
175
+ const pointRe = new RegExp('^(?:' + indent.replace(/[.*+?^${}()|[\]\\]/g, '\\$&') + ')*(?:\\*|\\d+\\.)');
176
+ return pointRe.test(line);
177
+ }
178
+ function bulletPointLines(lines, indent) {
179
+ return lines.split('\n').filter(Boolean).map(line => isPointedLine(line, indent) ? line : ' '.repeat(BULLET_POINT.length) + line).join('\n');
180
+ }
181
+ function numberedLines(lines, indent) {
182
+ let num = 0;
183
+ return lines.split('\n').filter(Boolean).map(line => {
184
+ if (isPointedLine(line, indent)) {
185
+ num++;
186
+ return line.replace(BULLET_POINT, `${String(num)}. `);
187
+ }
188
+ return ' '.repeat(`${String(num)}. `.length) + line;
189
+ }).join('\n');
190
+ }
191
+ function fixNestedLists(body, indent) {
192
+ const pointRe = '(?:\\*|\\d+\\.)';
193
+ const regex = new RegExp('(\\S(?: | )?)' +
194
+ '((?:' + indent.replace(/[.*+?^${}()|[\]\\]/g, '\\$&') + ')+)' +
195
+ '(' + pointRe + '(?:.*)+)$', 'gm');
196
+ return body.replace(regex, '$1\n' + indent + '$2$3');
197
+ }
198
+ // ---------------------------------------------------------------------------
199
+ // Table helpers
200
+ // ---------------------------------------------------------------------------
201
+ function generateTableRow(text, escape) {
202
+ if (!text)
203
+ return [];
204
+ const esc = escape || identity;
205
+ const lines = esc(text).split('\n');
206
+ const data = [];
207
+ for (const line of lines) {
208
+ if (!line)
209
+ continue;
210
+ const parsed = line.replace(TABLE_ROW_WRAP_RE, '').split(TABLE_CELL_SPLIT);
211
+ data.push(parsed.slice(0, -1));
212
+ }
213
+ return data;
214
+ }
215
+ // ---------------------------------------------------------------------------
216
+ // Renderer
217
+ // ---------------------------------------------------------------------------
218
+ class TerminalMarkdownRenderer {
219
+ o;
220
+ tab;
221
+ tableSettings;
222
+ transform;
223
+ // Set by marked extension wrapper before each call
224
+ parser = { parse: () => '', parseInline: () => '' };
225
+ options = {};
226
+ constructor(opts) {
227
+ this.o = { ...defaultOptions, ...opts };
228
+ this.tab = typeof this.o.tab === 'number' ? ' '.repeat(this.o.tab) : ' ';
229
+ this.tableSettings = this.o.tableOptions;
230
+ const unescape = this.o.unescape ? unescapeEntities : identity;
231
+ this.transform = (s) => undoColon(unescape(s));
232
+ }
233
+ space() { return ''; }
234
+ text(token) {
235
+ const t = typeof token === 'object' ? (token.text ?? '') : token;
236
+ return this.o.text(t);
237
+ }
238
+ code(token, lang) {
239
+ let code;
240
+ if (typeof token === 'object') {
241
+ lang = token.lang;
242
+ code = token.text;
243
+ }
244
+ else {
245
+ code = token;
246
+ }
247
+ code = fixHardReturn(code, this.o.reflowText);
248
+ return section(indentify(this.tab, highlightCode(code, lang, this.o.code)));
249
+ }
250
+ blockquote(token) {
251
+ let quote;
252
+ if (typeof token === 'object' && token.tokens) {
253
+ quote = this.parser.parse(token.tokens);
254
+ }
255
+ else {
256
+ quote = typeof token === 'string' ? token : '';
257
+ }
258
+ return section(this.o.blockquote(indentify(this.tab, quote.trim())));
259
+ }
260
+ html(token) {
261
+ const t = typeof token === 'object' ? (token.text ?? '') : token;
262
+ return this.o.html(t);
263
+ }
264
+ heading(token, level) {
265
+ let text;
266
+ if (typeof token === 'object' && token.tokens) {
267
+ level = token.depth;
268
+ text = this.parser.parseInline(token.tokens);
269
+ }
270
+ else {
271
+ text = typeof token === 'string' ? token : '';
272
+ }
273
+ text = this.transform(text);
274
+ const prefix = this.o.showSectionPrefix ? '#'.repeat(level ?? 1) + ' ' : '';
275
+ text = prefix + text;
276
+ if (this.o.reflowText)
277
+ text = reflowText(text, this.o.width, !!this.options.gfm);
278
+ return section(level === 1 ? this.o.firstHeading(text) : this.o.heading(text));
279
+ }
280
+ hr() {
281
+ return section(this.o.hr(hr('─', this.o.reflowText ? this.o.width : undefined)));
282
+ }
283
+ list(token, ordered) {
284
+ let body;
285
+ if (typeof token === 'object' && token.items) {
286
+ ordered = token.ordered;
287
+ body = '';
288
+ for (const item of token.items) {
289
+ body += this.listitem(item);
290
+ }
291
+ }
292
+ else {
293
+ body = typeof token === 'string' ? token : '';
294
+ }
295
+ body = body.trim();
296
+ body = ordered ? numberedLines(body, this.tab) : bulletPointLines(body, this.tab);
297
+ return section(fixNestedLists(indentLines(this.tab, body), this.tab));
298
+ }
299
+ listitem(token) {
300
+ let text;
301
+ if (typeof token === 'object' && token.tokens) {
302
+ const item = token;
303
+ text = '';
304
+ if (item.task) {
305
+ const cb = item.checked ? '[X] ' : '[ ] ';
306
+ text += cb;
307
+ }
308
+ text += this.parser.parse(item.tokens ?? [], !!item.loose);
309
+ }
310
+ else {
311
+ text = typeof token === 'string' ? token : '';
312
+ }
313
+ const transform = (s) => this.o.listitem(this.transform(s));
314
+ const isNested = text.includes('\n');
315
+ if (isNested)
316
+ text = text.trim();
317
+ return '\n' + BULLET_POINT + transform(text);
318
+ }
319
+ checkbox(token) {
320
+ const checked = typeof token === 'object' ? token.checked : token;
321
+ return '[' + (checked ? 'X' : ' ') + '] ';
322
+ }
323
+ paragraph(token) {
324
+ let text;
325
+ if (typeof token === 'object' && token.tokens) {
326
+ text = this.parser.parseInline(token.tokens);
327
+ }
328
+ else {
329
+ text = typeof token === 'string' ? token : '';
330
+ }
331
+ text = this.o.paragraph(this.transform(text));
332
+ if (this.o.reflowText)
333
+ text = reflowText(text, this.o.width, !!this.options.gfm);
334
+ return section(text);
335
+ }
336
+ table(token, body) {
337
+ let header;
338
+ if (typeof token === 'object' && token.header) {
339
+ header = '';
340
+ for (const cell of token.header) {
341
+ header += this.parser.parseInline(cell.tokens) + TABLE_CELL_SPLIT;
342
+ }
343
+ header = TABLE_ROW_WRAP + header + TABLE_ROW_WRAP + '\n';
344
+ body = '';
345
+ for (const row of token.rows ?? []) {
346
+ let rowStr = '';
347
+ for (const cell of row) {
348
+ rowStr += this.parser.parseInline(cell.tokens) + TABLE_CELL_SPLIT;
349
+ }
350
+ body += TABLE_ROW_WRAP + rowStr + TABLE_ROW_WRAP + '\n';
351
+ }
352
+ }
353
+ else {
354
+ header = typeof token === 'string' ? token : '';
355
+ }
356
+ const table = new Table({
357
+ head: generateTableRow(header)[0] ?? [],
358
+ ...this.tableSettings,
359
+ });
360
+ for (const row of generateTableRow(body ?? '', this.transform)) {
361
+ table.push(row);
362
+ }
363
+ return section(this.o.table(table.toString()));
364
+ }
365
+ tablerow(token) {
366
+ const content = typeof token === 'object' ? (token.text ?? '') : token;
367
+ return TABLE_ROW_WRAP + content + TABLE_ROW_WRAP + '\n';
368
+ }
369
+ tablecell(token) {
370
+ if (typeof token === 'object' && token.tokens) {
371
+ return this.parser.parseInline(token.tokens) + TABLE_CELL_SPLIT;
372
+ }
373
+ return (typeof token === 'string' ? token : '') + TABLE_CELL_SPLIT;
374
+ }
375
+ strong(token) {
376
+ let text;
377
+ if (typeof token === 'object' && token.tokens) {
378
+ text = this.parser.parseInline(token.tokens);
379
+ }
380
+ else {
381
+ text = typeof token === 'string' ? token : '';
382
+ }
383
+ return this.o.strong(text);
384
+ }
385
+ em(token) {
386
+ let text;
387
+ if (typeof token === 'object' && token.tokens) {
388
+ text = this.parser.parseInline(token.tokens);
389
+ }
390
+ else {
391
+ text = typeof token === 'string' ? token : '';
392
+ }
393
+ return this.o.em(fixHardReturn(text, this.o.reflowText));
394
+ }
395
+ codespan(token) {
396
+ let text = typeof token === 'object' ? (token.text ?? '') : token;
397
+ text = fixHardReturn(text, this.o.reflowText);
398
+ return this.o.codespan(text.replace(/:/g, COLON_REPLACER));
399
+ }
400
+ br() {
401
+ return this.o.reflowText ? HARD_RETURN : '\n';
402
+ }
403
+ del(token) {
404
+ let text;
405
+ if (typeof token === 'object' && token.tokens) {
406
+ text = this.parser.parseInline(token.tokens);
407
+ }
408
+ else {
409
+ text = typeof token === 'string' ? token : '';
410
+ }
411
+ return this.o.del(text);
412
+ }
413
+ link(token, _title, _text) {
414
+ let href;
415
+ let text;
416
+ if (typeof token === 'object' && token.tokens) {
417
+ href = token.href ?? '';
418
+ text = this.parser.parseInline(token.tokens);
419
+ }
420
+ else {
421
+ href = typeof token === 'string' ? token : '';
422
+ text = typeof _text === 'string' ? _text : '';
423
+ }
424
+ const hasText = text && text !== href;
425
+ let out = '';
426
+ if (supportsHyperlinks.stdout) {
427
+ const linkText = this.o.href(text || href);
428
+ out = ansiEscapes.link(linkText, href);
429
+ }
430
+ else {
431
+ if (hasText)
432
+ out += text + ' (';
433
+ out += this.o.href(href);
434
+ if (hasText)
435
+ out += ')';
436
+ }
437
+ return this.o.link(out);
438
+ }
439
+ image(token, title, text) {
440
+ if (typeof token === 'object') {
441
+ title = token.title;
442
+ text = token.text;
443
+ token = token.href ?? '';
444
+ }
445
+ let out = '![' + (text || '');
446
+ if (title)
447
+ out += ' – ' + title;
448
+ return out + '](' + token + ')\n';
449
+ }
450
+ }
451
+ // ---------------------------------------------------------------------------
452
+ // Public API — same interface as `marked-terminal`
453
+ // ---------------------------------------------------------------------------
454
+ /**
455
+ * Returns a marked extension that renders markdown to ANSI terminal output.
456
+ * Drop-in replacement for `markedTerminal` from the `marked-terminal` package.
457
+ */
458
+ export function markedTerminal(options) {
459
+ const r = new TerminalMarkdownRenderer(options ?? {});
460
+ const methods = [
461
+ 'space', 'text', 'code', 'blockquote', 'html', 'heading', 'hr',
462
+ 'list', 'listitem', 'checkbox', 'paragraph', 'table', 'tablerow',
463
+ 'tablecell', 'strong', 'em', 'codespan', 'br', 'del', 'link', 'image',
464
+ ];
465
+ const renderer = {};
466
+ for (const method of methods) {
467
+ renderer[method] = function (...args) {
468
+ r.options = this.options;
469
+ r.parser = this.parser;
470
+ return r[method](...args);
471
+ };
472
+ }
473
+ return { renderer, useNewRenderer: true };
474
+ }
@@ -10,7 +10,7 @@
10
10
  */
11
11
  import chalk from 'chalk';
12
12
  import { marked } from 'marked';
13
- import { markedTerminal } from 'marked-terminal';
13
+ import { markedTerminal } from '../../markdown-renderer.js';
14
14
  import { BaseOverlayV2 } from '../../base/overlay-base-v2.js';
15
15
  import { getCurrentTheme } from '../../../themes/index.js';
16
16
  import * as terminal from '../../terminal.js';
@@ -92,7 +92,6 @@ function getThemedMarkedOptions(termWidth) {
92
92
  }
93
93
  function renderMarkdownSync(content, termWidth) {
94
94
  const options = getThemedMarkedOptions(termWidth);
95
- // eslint-disable-next-line @typescript-eslint/no-unsafe-argument, @typescript-eslint/no-explicit-any
96
95
  marked.use(markedTerminal(options));
97
96
  const rendered = marked.parse(content, { async: false });
98
97
  return rendered.split('\n');
@@ -67,7 +67,7 @@ function setupAlternateScreenCleanup() {
67
67
  // Setup cleanup handlers once when module loads
68
68
  setupAlternateScreenCleanup();
69
69
  import { marked } from 'marked';
70
- import { markedTerminal } from 'marked-terminal';
70
+ import { markedTerminal } from '../../markdown-renderer.js';
71
71
  import { BaseOverlayV2 } from '../../base/overlay-base-v2.js';
72
72
  import { renderBorder, isCtrlC, isClose, isNavigateUp, isNavigateDown, isUpArrow, isDownArrow, isLeftArrow, isRightArrow, isHome, isEnd, isPageUp, isPageDown, isSpace, isBackspace, isDelete, isEnter, isTab, isEscape, isE, getPrintableChar, } from '../../base/index.js';
73
73
  import { documentRepository } from '../../../db/repositories/document-repository.js';
@@ -236,7 +236,6 @@ function postProcessInlineFormatting(text) {
236
236
  function renderMarkdownSync(content, termWidth) {
237
237
  const preprocessed = preprocessMarkdown(content);
238
238
  const options = getThemedMarkedOptions(termWidth);
239
- // eslint-disable-next-line @typescript-eslint/no-unsafe-argument, @typescript-eslint/no-explicit-any
240
239
  marked.use(markedTerminal(options));
241
240
  const rendered = marked.parse(preprocessed, { async: false });
242
241
  const postProcessed = postProcessInlineFormatting(rendered);
@@ -32,11 +32,17 @@ export interface HelpOverlayV2Options {
32
32
  }[];
33
33
  /** Get detailed info for a single command. */
34
34
  getCommand: (name: string) => CommandDetail | undefined;
35
+ /** Get custom skill and binding entries for the help list. */
36
+ getSkillEntries?: () => {
37
+ command: string;
38
+ description: string;
39
+ isBinding: boolean;
40
+ }[];
35
41
  }
36
42
  interface HelpItem {
37
43
  name: string;
38
44
  description: string;
39
- type: 'builtin' | 'custom';
45
+ type: 'builtin' | 'custom' | 'skill' | 'binding';
40
46
  /** For custom commands */
41
47
  prompt?: string;
42
48
  location?: 'project' | 'personal';
@@ -242,6 +242,17 @@ export class HelpOverlayV2 extends TabbedListOverlayV2 {
242
242
  location: cmd.location,
243
243
  });
244
244
  }
245
+ // Add custom skills and bindings
246
+ if (options.getSkillEntries) {
247
+ for (const entry of options.getSkillEntries()) {
248
+ items.push({
249
+ name: `/${entry.command}`,
250
+ description: entry.description,
251
+ type: entry.isBinding ? 'binding' : 'skill',
252
+ commandName: entry.command,
253
+ });
254
+ }
255
+ }
245
256
  super({
246
257
  title: VERSION,
247
258
  tabs: TABS,
@@ -253,7 +264,7 @@ export class HelpOverlayV2 extends TabbedListOverlayV2 {
253
264
  if (tabId === 'builtin')
254
265
  return item.type === 'builtin';
255
266
  if (tabId === 'custom')
256
- return item.type === 'custom';
267
+ return item.type === 'custom' || item.type === 'skill' || item.type === 'binding';
257
268
  return true;
258
269
  },
259
270
  getSearchText: (item) => `${item.name} ${item.description}`,
@@ -263,7 +274,13 @@ export class HelpOverlayV2 extends TabbedListOverlayV2 {
263
274
  ? styles.primary(item.name.padEnd(20))
264
275
  : styles.muted(item.name.padEnd(20));
265
276
  const desc = styles.muted(item.description);
266
- const badge = item.type === 'custom' ? styles.muted(' (custom)') : '';
277
+ let badge = '';
278
+ if (item.type === 'custom')
279
+ badge = styles.muted(' (custom)');
280
+ else if (item.type === 'skill')
281
+ badge = styles.secondary(' (skill)');
282
+ else if (item.type === 'binding')
283
+ badge = styles.secondary(' (binding)');
267
284
  return prefix + name + desc + badge;
268
285
  },
269
286
  footerHints: (searchMode) => {
@@ -0,0 +1,91 @@
1
+ /**
2
+ * Skill Detail Overlay V2
3
+ *
4
+ * Fullscreen SKILL.md viewer AND editor with vim-style navigation.
5
+ * Copied from DocumentDetailOverlayV2 — adapted for file-based skills.
6
+ *
7
+ * Modes:
8
+ * - View: rendered markdown, scrollable (j/k, g/G, PgUp/PgDn)
9
+ * - Edit: raw markdown editing with cursor, line numbers, syntax highlighting
10
+ * - Diff: side-by-side diff against upstream SDK version (forked skills only)
11
+ *
12
+ * Features:
13
+ * - Full-screen viewing with markdown rendering
14
+ * - Vim-style navigation
15
+ * - Edit mode with raw markdown editing
16
+ * - Save to disk (Ctrl+S) with validation warnings
17
+ * - Dirty check with save prompt on exit
18
+ * - Diff view for forked skills (D key)
19
+ */
20
+ import { BaseOverlayV2 } from '../../base/overlay-base-v2.js';
21
+ import type { RenderContext, OverlayAction, KeyEvent } from '../types.js';
22
+ import { type ForkedFromMarker, type SkillDiffLine } from '@compilr-dev/sdk';
23
+ type SkillDetailMode = 'view' | 'edit' | 'diff';
24
+ interface SkillDetailState {
25
+ mode: SkillDetailMode;
26
+ contentLines: string[];
27
+ scrollOffset: number;
28
+ rawLines: string[];
29
+ cursorLine: number;
30
+ cursorColumn: number;
31
+ editScrollOffset: number;
32
+ isDirty: boolean;
33
+ originalContent: string;
34
+ showSavePrompt: boolean;
35
+ saveMessage: string | null;
36
+ diffLines: SkillDiffLine[] | null;
37
+ diffScrollOffset: number;
38
+ skillName: string;
39
+ scope: string;
40
+ forkedFrom: ForkedFromMarker | null;
41
+ boundCommands: string[];
42
+ validationWarnings: string[];
43
+ }
44
+ export interface SkillDetailResult {
45
+ /** Whether to go back to the list */
46
+ goBack: boolean;
47
+ /** Whether content was modified (caller should reload) */
48
+ modified: boolean;
49
+ }
50
+ export declare class SkillDetailOverlayV2 extends BaseOverlayV2<SkillDetailState, SkillDetailResult> {
51
+ readonly type: "inline";
52
+ readonly id = "skill-detail-overlay-v2";
53
+ readonly usesAlternateScreen = true;
54
+ private readonly filePath;
55
+ constructor(options: {
56
+ filePath: string;
57
+ skillName: string;
58
+ scope: string;
59
+ content: string;
60
+ forkedFrom?: ForkedFromMarker;
61
+ boundCommands?: string[];
62
+ });
63
+ onMount(): void;
64
+ onUnmount(): void;
65
+ protected renderContent(context: RenderContext): string[];
66
+ private renderViewMode;
67
+ private renderEditMode;
68
+ private renderLineWithCursor;
69
+ private ensureCursorVisible;
70
+ private renderDiffMode;
71
+ handleKey(key: KeyEvent): OverlayAction<SkillDetailResult>;
72
+ private handleViewModeKey;
73
+ private handleDiffModeKey;
74
+ private handleEditModeKey;
75
+ private handleSavePromptKey;
76
+ private moveCursorUp;
77
+ private moveCursorDown;
78
+ private moveCursorLeft;
79
+ private moveCursorRight;
80
+ private clampCursorColumn;
81
+ private getCurrentLine;
82
+ private insertText;
83
+ private handleBackspace;
84
+ private handleDeleteKey;
85
+ private handleEnter;
86
+ private saveSkill;
87
+ private exitEditMode;
88
+ private closeWith;
89
+ private renderAction;
90
+ }
91
+ export {};