@compilr-dev/cli 0.8.10 → 0.9.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/CHANGELOG.md +35 -0
  2. package/dist/agent.d.ts +9 -0
  3. package/dist/agent.js +1 -0
  4. package/dist/commands-v2/handlers/project.js +88 -229
  5. package/dist/commands-v2/handlers/skill.js +10 -6
  6. package/dist/commands-v2/handlers/team.d.ts +19 -1
  7. package/dist/commands-v2/handlers/team.js +178 -3
  8. package/dist/compilr-diff-companion.vsix +0 -0
  9. package/dist/index.js +55 -3
  10. package/dist/multi-agent/agent-edit.d.ts +75 -0
  11. package/dist/multi-agent/agent-edit.js +84 -0
  12. package/dist/multi-agent/capability-loader.d.ts +15 -5
  13. package/dist/multi-agent/capability-loader.js +18 -8
  14. package/dist/multi-agent/custom-agents.d.ts +12 -0
  15. package/dist/multi-agent/custom-agents.js +22 -1
  16. package/dist/repl-helpers.d.ts +6 -1
  17. package/dist/repl-helpers.js +14 -7
  18. package/dist/repl-v2.d.ts +10 -0
  19. package/dist/repl-v2.js +43 -2
  20. package/dist/ui/overlay/impl/agent-edit-overlay-v2.d.ts +105 -0
  21. package/dist/ui/overlay/impl/agent-edit-overlay-v2.js +364 -0
  22. package/dist/ui/overlay/impl/custom-agent-form-overlay-v2.d.ts +1 -8
  23. package/dist/ui/overlay/impl/custom-agent-form-overlay-v2.js +16 -120
  24. package/dist/ui/overlay/impl/project-edit-overlay-v2.d.ts +2 -1
  25. package/dist/ui/overlay/impl/project-edit-overlay-v2.js +26 -19
  26. package/dist/ui/overlay/impl/skills-overlay-v2.d.ts +42 -2
  27. package/dist/ui/overlay/impl/skills-overlay-v2.js +238 -185
  28. package/dist/ui/overlay/impl/team-overlay-v2.d.ts +1 -2
  29. package/dist/ui/overlay/impl/team-overlay-v2.js +6 -9
  30. package/dist/ui/terminal-types.d.ts +8 -0
  31. package/package.json +2 -2
@@ -17,7 +17,7 @@
17
17
  import { promises as fs, readFileSync } from 'node:fs';
18
18
  import * as terminal from '../../terminal.js';
19
19
  import { TabbedListOverlayV2, BaseScreen, stay, popScreen, closeOverlay, isEscape, isCtrlC, isEnter, isUpArrow, isDownArrow, renderBorder, truncate, isPrintable, getPrintableChar, } from '../../base/index.js';
20
- import { isReservedSkillName, parseSkillMarkdown, loadSkillsFromDir, platformSkills, builtinSkills, validateSkillQuality, buildForkContent, buildNewSkillContent, } from '@compilr-dev/sdk';
20
+ import { isReservedMacroName, parseSkillMarkdown, loadSkillsFromDir, buildMacroList, platformMacros, builtinSkills, validateSkillQuality, buildForkContent, buildNewSkillContent, skillReachability, patchSkillFrontmatter, } from '@compilr-dev/sdk';
21
21
  import { getSkillsDir, getSkillFolder, getSkillFile, ensureSkillsDir, readScopeConfig, writeScopeConfig, getScopeConfigPath, isValidSkillName, } from '../../../utils/skill-paths.js';
22
22
  // =============================================================================
23
23
  // Constants
@@ -27,7 +27,13 @@ const NAME_WIDTH = 24;
27
27
  const BODY_PREVIEW_LINES = 10;
28
28
  const SKILL_TABS = [
29
29
  { id: 'custom', label: 'Custom' },
30
- { id: 'builtin', label: 'Built-in' },
30
+ /*
31
+ ⚠️ NO "Built-in" TAB. It listed MACROS — shipped prompts — inside a manager for SKILLS, which
32
+ are a different population with a different file format and a different way of being chosen.
33
+ Desktop removed the same section; a rule honoured in one host and not the other is a bug in
34
+ both. The macros have not gone: they are the fork source in "New skill → from a command",
35
+ which is where forking belongs, because forking is only ever the start of creating a skill.
36
+ */
31
37
  { id: 'bindings', label: 'Bindings' },
32
38
  ];
33
39
  // =============================================================================
@@ -62,7 +68,7 @@ async function loadCustomSkills(projectPath) {
62
68
  name: skill.name,
63
69
  description: skill.description,
64
70
  scope: 'project',
65
- enabled: skill.enabled !== false,
71
+ reach: skillReachability(skill),
66
72
  issues,
67
73
  skill,
68
74
  filePath,
@@ -88,7 +94,7 @@ async function loadCustomSkills(projectPath) {
88
94
  name: skill.name,
89
95
  description: skill.description,
90
96
  scope: 'user',
91
- enabled: skill.enabled !== false,
97
+ reach: skillReachability(skill),
92
98
  issues,
93
99
  skill,
94
100
  filePath,
@@ -104,32 +110,37 @@ async function loadCustomSkills(projectPath) {
104
110
  }
105
111
  return items;
106
112
  }
107
- function loadBuiltinSkills() {
108
- const seen = new Set();
109
- const items = [];
110
- const addSkill = (skill) => {
111
- if (seen.has(skill.name))
112
- return;
113
- seen.add(skill.name);
114
- items.push({
115
- name: skill.name,
116
- description: skill.description,
117
- scope: 'sdk',
118
- enabled: true,
119
- issues: [],
120
- skill: null,
121
- filePath: null,
122
- bodyPreview: null,
123
- boundCommands: [],
124
- });
125
- };
126
- // SDK platform skills first (have updated descriptions)
127
- for (const skill of platformSkills)
128
- addSkill(skill);
129
- // agents library built-in skills (deduplicated)
130
- for (const skill of builtinSkills)
131
- addSkill(skill);
132
- return items;
113
+ /**
114
+ * The shipped macros this tab offers.
115
+ *
116
+ * ⚠️ IT USED TO OFFER EIGHT IT CANNOT RUN. This walked `platformMacros` directly and added every
117
+ * one, including the canvas family — which `getSkillPrompt` REFUSES, because a terminal cannot
118
+ * draw a canvas. So the Built-in tab listed `canvas`, `infographic`, `carousel`, `board`,
119
+ * `canvas-styles` and the three reference ones, and selecting any of them got nothing. That is
120
+ * the same "offered, then answers Unknown command" defect the CLI had already fixed for
121
+ * disabled skills, in a second place nobody checked.
122
+ *
123
+ * It now asks the SDK, declaring what this host can render. The resolver's refusal and this
124
+ * list are therefore two consequences of one rule rather than two rules that can disagree.
125
+ */
126
+ export function loadBuiltinMacros(projectPath, sources) {
127
+ return buildMacroList({
128
+ projectPath,
129
+ sources,
130
+ capabilities: { canvas: false },
131
+ })
132
+ .filter((entry) => !entry.label.startsWith('/'))
133
+ .map((entry) => ({
134
+ name: entry.id,
135
+ description: entry.description,
136
+ scope: 'sdk',
137
+ reach: { byModel: true, byUser: true },
138
+ issues: [],
139
+ skill: null,
140
+ filePath: null,
141
+ bodyPreview: null,
142
+ boundCommands: [],
143
+ }));
133
144
  }
134
145
  async function loadBindingItems() {
135
146
  const items = [];
@@ -142,7 +153,7 @@ async function loadBindingItems() {
142
153
  name: skillName,
143
154
  description: `/${cmd} → ${skillName}`,
144
155
  scope: 'binding',
145
- enabled: true,
156
+ reach: { byModel: true, byUser: true },
146
157
  issues: [],
147
158
  skill: null,
148
159
  filePath: null,
@@ -180,20 +191,6 @@ function matchFrontmatter(content) {
180
191
  }
181
192
  return null;
182
193
  }
183
- function setEnabledFlag(content, enabled) {
184
- const fm = matchFrontmatter(content);
185
- if (!fm)
186
- return content;
187
- const fmLines = fm.frontmatter.split('\n');
188
- const idx = fmLines.findIndex((l) => /^enabled\s*:/.test(l));
189
- if (idx >= 0) {
190
- fmLines[idx] = `enabled: ${String(enabled)}`;
191
- }
192
- else {
193
- fmLines.push(`enabled: ${String(enabled)}`);
194
- }
195
- return ['---', ...fmLines, '---', fm.body].join('\n');
196
- }
197
194
  function replaceFrontmatterName(content, newName) {
198
195
  const fm = matchFrontmatter(content);
199
196
  if (!fm)
@@ -213,19 +210,32 @@ async function pathExists(p) {
213
210
  class NewSkillScreen extends BaseScreen {
214
211
  styles;
215
212
  onCreated;
216
- phase = 'name';
213
+ macros;
214
+ phase = 'start';
215
+ route = 'blank';
217
216
  nameInput = '';
218
217
  selectedScope = 'project';
219
218
  error = '';
220
219
  hasProject;
221
- constructor(styles, onCreated, hasProject) {
220
+ /** The macro being forked, or null for a blank skill. */
221
+ picked = null;
222
+ query = '';
223
+ pickIndex = 0;
224
+ constructor(styles, onCreated, hasProject, macros = []) {
222
225
  super();
223
226
  this.styles = styles;
224
227
  this.onCreated = onCreated;
228
+ this.macros = macros;
225
229
  this.hasProject = hasProject;
226
230
  if (!hasProject)
227
231
  this.selectedScope = 'user';
228
232
  }
233
+ get filtered() {
234
+ const q = this.query.trim().toLowerCase();
235
+ if (!q)
236
+ return this.macros;
237
+ return this.macros.filter((m) => m.name.toLowerCase().includes(q) || m.description.toLowerCase().includes(q));
238
+ }
229
239
  render() {
230
240
  const s = this.styles;
231
241
  const cols = terminal.getTerminalWidth();
@@ -234,7 +244,45 @@ class NewSkillScreen extends BaseScreen {
234
244
  lines.push(border);
235
245
  lines.push(` ${s.primaryBold('New Skill')}`);
236
246
  lines.push('');
247
+ if (this.phase === 'start') {
248
+ lines.push(` ${s.muted('Start from:')}`);
249
+ const rows = [
250
+ ['blank', 'Blank', 'an empty skill you write yourself'],
251
+ ['command', 'From a command', 'a copy of a built-in, yours to change'],
252
+ ];
253
+ for (const [id, label, hint] of rows) {
254
+ const on = this.route === id;
255
+ lines.push(` ${on ? s.primary('> ') : ' '}${on ? s.primary(label) : s.muted(label)} ${s.muted(`(${hint})`)}`);
256
+ }
257
+ lines.push('');
258
+ lines.push(` ${s.muted('↑↓ Select · Enter to continue · Esc to cancel')}`);
259
+ lines.push(border);
260
+ return lines;
261
+ }
262
+ if (this.phase === 'pick') {
263
+ lines.push(` ${s.muted('Which command? Type to filter.')}`);
264
+ lines.push(` ${s.primary('> ')}${this.query}${s.primary('█')}`);
265
+ lines.push('');
266
+ const rows = this.filtered.slice(0, 8);
267
+ if (rows.length === 0) {
268
+ lines.push(` ${s.muted('No command matches that.')}`);
269
+ }
270
+ for (const [i, m] of rows.entries()) {
271
+ const on = i === this.pickIndex;
272
+ const name = m.name.padEnd(18);
273
+ const desc = truncate(m.description.replace(/\s+/g, ' ').trim(), Math.max(10, cols - 26));
274
+ lines.push(` ${on ? s.primary('> ') : ' '}${on ? s.primary(name) : s.muted(name)}${s.muted(desc)}`);
275
+ }
276
+ lines.push('');
277
+ lines.push(` ${s.muted('↑↓ Select · Enter to choose · Esc to go back')}`);
278
+ lines.push(border);
279
+ return lines;
280
+ }
237
281
  if (this.phase === 'name') {
282
+ if (this.picked) {
283
+ lines.push(` ${s.muted('Forking:')} ${s.secondary(this.picked)}`);
284
+ lines.push('');
285
+ }
238
286
  lines.push(` ${s.muted('Name (lowercase letters, digits, hyphens):')}`);
239
287
  lines.push(` ${s.primary('> ')}${this.nameInput}${s.primary('█')}`);
240
288
  if (this.error) {
@@ -264,8 +312,81 @@ class NewSkillScreen extends BaseScreen {
264
312
  handleKey(data) {
265
313
  if (isCtrlC(data))
266
314
  return closeOverlay({ dismissed: true });
267
- if (isEscape(data))
315
+ if (isEscape(data)) {
316
+ // Esc steps BACK through the wizard rather than discarding it from the middle.
317
+ if (this.phase === 'pick') {
318
+ this.phase = 'start';
319
+ return stay(true);
320
+ }
321
+ if (this.phase === 'name' && this.route === 'command') {
322
+ this.phase = 'pick';
323
+ return stay(true);
324
+ }
325
+ if (this.phase === 'name') {
326
+ this.phase = 'start';
327
+ return stay(true);
328
+ }
329
+ if (this.phase === 'scope') {
330
+ this.phase = 'name';
331
+ return stay(true);
332
+ }
268
333
  return popScreen();
334
+ }
335
+ if (this.phase === 'start') {
336
+ if (isEnter(data)) {
337
+ if (this.route === 'blank') {
338
+ this.picked = null;
339
+ this.phase = 'name';
340
+ }
341
+ else {
342
+ this.phase = 'pick';
343
+ this.pickIndex = 0;
344
+ }
345
+ return stay(true);
346
+ }
347
+ if (isUpArrow(data) || isDownArrow(data)) {
348
+ this.route = this.route === 'blank' ? 'command' : 'blank';
349
+ return stay(true);
350
+ }
351
+ return stay(false);
352
+ }
353
+ if (this.phase === 'pick') {
354
+ const rows = this.filtered;
355
+ if (isEnter(data)) {
356
+ // Guarded on LENGTH, not on the element: `rows[i]` is not typed as possibly-undefined,
357
+ // so `if (!chosen)` lints as always-falsy while the empty list is entirely real.
358
+ if (rows.length === 0)
359
+ return stay(false);
360
+ const chosen = rows[Math.min(this.pickIndex, rows.length - 1)];
361
+ this.picked = chosen.name;
362
+ /*
363
+ ⚠️ THE FORK NAME MUST DIFFER FROM ITS SOURCE, or the copy shadows the built-in it came
364
+ from. Seeded, not enforced here — the name phase validates it like any other.
365
+ */
366
+ this.nameInput = `${chosen.name}-custom`;
367
+ this.phase = 'name';
368
+ return stay(true);
369
+ }
370
+ if (isUpArrow(data)) {
371
+ this.pickIndex = Math.max(0, this.pickIndex - 1);
372
+ return stay(true);
373
+ }
374
+ if (isDownArrow(data)) {
375
+ this.pickIndex = Math.min(Math.max(0, Math.min(rows.length, 8) - 1), this.pickIndex + 1);
376
+ return stay(true);
377
+ }
378
+ if (data[0] === 127 || data[0] === 8) {
379
+ this.query = this.query.slice(0, -1);
380
+ this.pickIndex = 0;
381
+ return stay(true);
382
+ }
383
+ if (isPrintable(data)) {
384
+ this.query += getPrintableChar(data) ?? '';
385
+ this.pickIndex = 0;
386
+ return stay(true);
387
+ }
388
+ return stay(false);
389
+ }
269
390
  if (this.phase === 'name') {
270
391
  if (isEnter(data)) {
271
392
  const name = this.nameInput.trim();
@@ -273,11 +394,15 @@ class NewSkillScreen extends BaseScreen {
273
394
  this.error = 'Name cannot be empty.';
274
395
  return stay(true);
275
396
  }
397
+ if (this.picked !== null && name === this.picked) {
398
+ this.error = 'A fork must be named differently from the command it copies.';
399
+ return stay(true);
400
+ }
276
401
  if (!isValidSkillName(name)) {
277
402
  this.error = 'Use lowercase letters, digits, and hyphens only.';
278
403
  return stay(true);
279
404
  }
280
- if (isReservedSkillName(name)) {
405
+ if (isReservedMacroName(name)) {
281
406
  this.error = `'${name}' is reserved by SDK. Use a different name.`;
282
407
  return stay(true);
283
408
  }
@@ -301,7 +426,7 @@ class NewSkillScreen extends BaseScreen {
301
426
  }
302
427
  // Scope selection phase
303
428
  if (isEnter(data)) {
304
- void this.onCreated(this.nameInput.trim(), this.selectedScope);
429
+ void this.onCreated(this.nameInput.trim(), this.selectedScope, this.picked);
305
430
  return popScreen();
306
431
  }
307
432
  if (isUpArrow(data) || isDownArrow(data)) {
@@ -324,7 +449,7 @@ class NewSkillScreen extends BaseScreen {
324
449
  return stay(false);
325
450
  }
326
451
  getMinHeight() {
327
- return 12;
452
+ return this.phase === 'pick' ? 18 : 12;
328
453
  }
329
454
  }
330
455
  // =============================================================================
@@ -377,7 +502,7 @@ class RenameScreen extends BaseScreen {
377
502
  this.error = 'Use lowercase letters, digits, and hyphens only.';
378
503
  return stay(true);
379
504
  }
380
- if (isReservedSkillName(name)) {
505
+ if (isReservedMacroName(name)) {
381
506
  this.error = `'${name}' is reserved by SDK.`;
382
507
  return stay(true);
383
508
  }
@@ -448,111 +573,6 @@ class DeleteConfirmScreen extends BaseScreen {
448
573
  return 8;
449
574
  }
450
575
  }
451
- // =============================================================================
452
- // Unbind Confirmation Screen
453
- // =============================================================================
454
- // UnbindConfirmScreen removed — replaced by SkillBindingScreen
455
- // =============================================================================
456
- // Fork Screen (for SDK skills)
457
- // =============================================================================
458
- class ForkScreen extends BaseScreen {
459
- sdkSkillName;
460
- styles;
461
- onForked;
462
- nameInput;
463
- selectedScope = 'project';
464
- error = '';
465
- hasProject;
466
- constructor(sdkSkillName, styles, hasProject, onForked) {
467
- super();
468
- this.sdkSkillName = sdkSkillName;
469
- this.styles = styles;
470
- this.onForked = onForked;
471
- this.hasProject = hasProject;
472
- this.nameInput = sdkSkillName + '-custom';
473
- if (!hasProject)
474
- this.selectedScope = 'user';
475
- }
476
- render() {
477
- const s = this.styles;
478
- const cols = terminal.getTerminalWidth();
479
- const border = renderBorder(cols, s);
480
- const lines = [];
481
- lines.push(border);
482
- lines.push(` ${s.primaryBold('Fork SDK Skill')}`);
483
- lines.push(` ${s.muted('Source:')} ${s.secondary(this.sdkSkillName)} ${s.muted('(SDK)')}`);
484
- lines.push('');
485
- lines.push(` ${s.muted('New name (must be different from SDK name):')}`);
486
- lines.push(` ${s.primary('> ')}${this.nameInput}${s.primary('█')}`);
487
- if (this.error) {
488
- lines.push(` ${s.warning(this.error)}`);
489
- }
490
- lines.push('');
491
- lines.push(` ${s.muted('Scope:')}`);
492
- if (this.hasProject) {
493
- const pCursor = this.selectedScope === 'project' ? s.primary('> ') : ' ';
494
- const pLabel = this.selectedScope === 'project' ? s.primary('project') : s.muted('project');
495
- lines.push(` ${pCursor}${pLabel}`);
496
- }
497
- const uCursor = this.selectedScope === 'user' ? s.primary('> ') : ' ';
498
- const uLabel = this.selectedScope === 'user' ? s.primary('user') : s.muted('user');
499
- lines.push(` ${uCursor}${uLabel}`);
500
- lines.push('');
501
- lines.push(` ${s.muted('Tab Switch scope · Enter Fork · Esc Cancel')}`);
502
- lines.push(border);
503
- return lines;
504
- }
505
- handleKey(data) {
506
- if (isCtrlC(data))
507
- return closeOverlay({ dismissed: true });
508
- if (isEscape(data))
509
- return popScreen();
510
- if (isEnter(data)) {
511
- const name = this.nameInput.trim();
512
- if (!name) {
513
- this.error = 'Name cannot be empty.';
514
- return stay(true);
515
- }
516
- if (!isValidSkillName(name)) {
517
- this.error = 'Use lowercase letters, digits, and hyphens.';
518
- return stay(true);
519
- }
520
- if (isReservedSkillName(name)) {
521
- this.error = `'${name}' is reserved. Choose a different name.`;
522
- return stay(true);
523
- }
524
- if (name === this.sdkSkillName) {
525
- this.error = 'Fork name must differ from SDK name.';
526
- return stay(true);
527
- }
528
- void this.onForked(name, this.selectedScope);
529
- return popScreen();
530
- }
531
- // Tab to switch scope
532
- if (data[0] === 0x09 && this.hasProject) {
533
- this.selectedScope = this.selectedScope === 'project' ? 'user' : 'project';
534
- return stay(true);
535
- }
536
- if (isUpArrow(data) || isDownArrow(data)) {
537
- if (this.hasProject) {
538
- this.selectedScope = this.selectedScope === 'project' ? 'user' : 'project';
539
- }
540
- return stay(true);
541
- }
542
- if (data[0] === 127 || data[0] === 8) {
543
- this.nameInput = this.nameInput.slice(0, -1);
544
- this.error = '';
545
- return stay(true);
546
- }
547
- if (isPrintable(data)) {
548
- this.nameInput += getPrintableChar(data) ?? '';
549
- this.error = '';
550
- return stay(true);
551
- }
552
- return stay(false);
553
- }
554
- getMinHeight() { return 14; }
555
- }
556
576
  class SkillBindingScreen extends BaseScreen {
557
577
  skillName;
558
578
  styles;
@@ -774,6 +794,7 @@ export class SkillsOverlayV2 extends TabbedListOverlayV2 {
774
794
  id = 'skills-overlay-v2';
775
795
  allItems = [];
776
796
  projectPath;
797
+ skillSources;
777
798
  onOpenDetail;
778
799
  constructor(options) {
779
800
  const projectPath = options.projectPath;
@@ -789,8 +810,6 @@ export class SkillsOverlayV2 extends TabbedListOverlayV2 {
789
810
  filterByTab: (item, tabId) => {
790
811
  if (tabId === 'custom')
791
812
  return item.scope !== 'sdk' && item.scope !== 'binding';
792
- if (tabId === 'builtin')
793
- return item.scope === 'sdk';
794
813
  if (tabId === 'bindings')
795
814
  return item.scope === 'binding';
796
815
  return true;
@@ -820,7 +839,19 @@ export class SkillsOverlayV2 extends TabbedListOverlayV2 {
820
839
  ? styles.primary(nameText)
821
840
  : styles.muted(nameText);
822
841
  // Disabled indicator
823
- const disabledTag = !item.enabled && isCustom ? styles.warning(' OFF') : '';
842
+ /*
843
+ ⚠️ THE REAL STATE, NOT A BOOLEAN. "OFF" could not distinguish a shelved skill from one an
844
+ agent may still choose on its own — two different files that behave differently.
845
+ */
846
+ const disabledTag = !isCustom
847
+ ? ''
848
+ : !item.reach.byModel && !item.reach.byUser
849
+ ? styles.warning(' SHELVED')
850
+ : !item.reach.byUser
851
+ ? styles.muted(' agents only')
852
+ : !item.reach.byModel
853
+ ? styles.muted(' /name only')
854
+ : '';
824
855
  // Scope badge
825
856
  let scopeBadge;
826
857
  if (item.scope === 'project')
@@ -830,7 +861,9 @@ export class SkillsOverlayV2 extends TabbedListOverlayV2 {
830
861
  else
831
862
  scopeBadge = styles.muted('[sdk]');
832
863
  // Description
833
- const badgeLen = item.scope.length + 2 + (disabledTag ? 4 : 0);
864
+ // eslint-disable-next-line no-control-regex
865
+ const tagLen = disabledTag.replace(/\u001b\[[0-9;]*m/g, '').length;
866
+ const badgeLen = item.scope.length + 2 + tagLen;
834
867
  const descWidth = Math.max(10, cols - NAME_WIDTH - badgeLen - 14);
835
868
  const desc = truncate(item.description.replace(/\s+/g, ' ').trim(), descWidth);
836
869
  const descColor = isSelected ? styles.secondary(desc) : styles.muted(desc);
@@ -845,18 +878,16 @@ export class SkillsOverlayV2 extends TabbedListOverlayV2 {
845
878
  if (searchMode) {
846
879
  return 'Type to filter · ↑↓/jk Navigate · Enter · Esc Exit search';
847
880
  }
848
- if (activeTabId === 'builtin') {
849
- return '←→ Tab · ↑↓/jk · / Search · f Fork · q/Esc Close';
850
- }
851
881
  if (activeTabId === 'bindings') {
852
882
  return '←→ Tab · ↑↓/jk · / Search · Enter Manage · d Remove · q/Esc Close';
853
883
  }
854
- return '←→ Tab · ↑↓/jk · / Search · Enter Open · n New · b Bindings · Space On/Off · d Del · q/Esc';
884
+ return '←→ Tab · ↑↓/jk · / Search · Enter Open · n New · b Bindings · Space Shelve · d Del · q/Esc';
855
885
  },
856
886
  emptyMessage: 'No custom skills yet. Press n to create one.',
857
887
  noResultsMessage: 'No skills match the search.',
858
888
  });
859
889
  this.projectPath = projectPath;
890
+ this.skillSources = options.skillSources;
860
891
  this.onOpenDetail = options.onOpenDetail;
861
892
  overlayRef = this; // eslint-disable-line @typescript-eslint/no-this-alias
862
893
  }
@@ -865,7 +896,6 @@ export class SkillsOverlayV2 extends TabbedListOverlayV2 {
865
896
  */
866
897
  async loadItems() {
867
898
  const custom = await loadCustomSkills(this.projectPath);
868
- const builtin = loadBuiltinSkills();
869
899
  const bindings = await loadBindingItems();
870
900
  // Sort: project first, then user (within custom), all alphabetical
871
901
  custom.sort((a, b) => {
@@ -873,17 +903,14 @@ export class SkillsOverlayV2 extends TabbedListOverlayV2 {
873
903
  return a.scope === 'project' ? -1 : 1;
874
904
  return a.name.localeCompare(b.name);
875
905
  });
876
- builtin.sort((a, b) => a.name.localeCompare(b.name));
877
906
  bindings.sort((a, b) => (a.bindingCommand ?? '').localeCompare(b.bindingCommand ?? ''));
878
- this.allItems = [...custom, ...builtin, ...bindings];
907
+ this.allItems = [...custom, ...bindings];
879
908
  this.state.items = this.allItems;
880
909
  // Re-filter: apply tab filter based on current tab index
881
910
  const tabId = SKILL_TABS[this.state.currentTab]?.id ?? 'custom';
882
911
  this.state.filteredItems = this.allItems.filter((item) => {
883
912
  if (tabId === 'custom')
884
913
  return item.scope !== 'sdk' && item.scope !== 'binding';
885
- if (tabId === 'builtin')
886
- return item.scope === 'sdk';
887
914
  if (tabId === 'bindings')
888
915
  return item.scope === 'binding';
889
916
  return true;
@@ -922,20 +949,27 @@ export class SkillsOverlayV2 extends TabbedListOverlayV2 {
922
949
  const ch = isPrintable(data) ? (getPrintableChar(data) ?? '') : '';
923
950
  // n — new skill
924
951
  if (ch === 'n') {
925
- const screen = new NewSkillScreen(this.getStyles(), async (name, scope) => {
926
- await this.createSkill(name, scope);
927
- }, !!this.projectPath);
952
+ const screen = new NewSkillScreen(this.getStyles(),
953
+ /*
954
+ ⚠️ `forkOf` MUST BE NAMED HERE. A callback written `(name, scope)` satisfies this
955
+ signature — TypeScript accepts fewer parameters — so dropping it compiles cleanly and
956
+ silently turns every fork into a blank skill.
957
+ */
958
+ async (name, scope, forkOf) => {
959
+ if (forkOf !== null)
960
+ await this.forkSkill(forkOf, name, scope);
961
+ else
962
+ await this.createSkill(name, scope);
963
+ }, !!this.projectPath,
964
+ // The same list the Built-in tab used to show, and the same rule about what this host runs.
965
+ loadBuiltinMacros(this.projectPath, this.skillSources).map((m) => ({
966
+ name: m.name,
967
+ description: m.description,
968
+ })));
928
969
  return { handled: true, pushScreen: screen };
929
970
  }
930
971
  if (!selectedItem)
931
972
  return { handled: false };
932
- // f — fork (SDK skills only)
933
- if (ch === 'f' && selectedItem.scope === 'sdk') {
934
- const screen = new ForkScreen(selectedItem.name, this.getStyles(), !!this.projectPath, async (newName, scope) => {
935
- await this.forkSkill(selectedItem.name, newName, scope);
936
- });
937
- return { handled: true, pushScreen: screen };
938
- }
939
973
  // b — open binding screen (custom skills only)
940
974
  if (ch === 'b' && selectedItem.scope !== 'sdk' && selectedItem.scope !== 'binding') {
941
975
  const screen = new SkillBindingScreen(selectedItem.name, this.getStyles(), async () => {
@@ -1005,14 +1039,33 @@ export class SkillsOverlayV2 extends TabbedListOverlayV2 {
1005
1039
  await this.loadItems();
1006
1040
  this.requestRender();
1007
1041
  }
1042
+ /**
1043
+ * Shelve a skill, or bring it back.
1044
+ *
1045
+ * ⚠️ WRITES THE TWO ANTHROPIC SWITCHES AND REMOVES THE LEGACY KEY (D-1), THROUGH THE SDK'S
1046
+ * PATCHER RATHER THAN A REGEX (D-2). The old version rewrote frontmatter line by line, which
1047
+ * destroys comments — measured on our own template: 17 lines to 7, all 9 comment lines to 0.
1048
+ *
1049
+ * One key for two switches is lossy by construction: a skill set to "agents only" in Desktop is
1050
+ * shelved and then restored to BOTH. That is why the list renders the real state beside the name
1051
+ * — the flattening is visible, not silent — and why the two-switch editor lives in the host that
1052
+ * has a document to put it in.
1053
+ */
1008
1054
  async toggleSkill(item) {
1009
1055
  if (!item.filePath)
1010
1056
  return;
1057
+ const shelved = !item.reach.byModel && !item.reach.byUser;
1058
+ const next = shelved
1059
+ ? { byModel: true, byUser: true }
1060
+ : { byModel: false, byUser: false };
1011
1061
  const content = await fs.readFile(item.filePath, 'utf-8');
1012
- const newEnabled = !item.enabled;
1013
- const updated = setEnabledFlag(content, newEnabled);
1014
- await fs.writeFile(item.filePath, updated);
1015
- item.enabled = newEnabled;
1062
+ await fs.writeFile(item.filePath, patchSkillFrontmatter(content, {
1063
+ 'disable-model-invocation': next.byModel ? null : true,
1064
+ 'user-invocable': next.byUser ? null : false,
1065
+ // The legacy key is deleted on every write, so a file touched here stops carrying it.
1066
+ enabled: null,
1067
+ }));
1068
+ item.reach = next;
1016
1069
  }
1017
1070
  async deleteSkill(item) {
1018
1071
  if (item.scope === 'sdk')
@@ -1056,7 +1109,7 @@ export class SkillsOverlayV2 extends TabbedListOverlayV2 {
1056
1109
  if (await pathExists(folder))
1057
1110
  return;
1058
1111
  // Find the SDK skill
1059
- const allSdk = [...platformSkills, ...builtinSkills];
1112
+ const allSdk = [...platformMacros, ...builtinSkills];
1060
1113
  const sdkSkill = allSdk.find((s) => s.name === sdkName);
1061
1114
  if (!sdkSkill)
1062
1115
  return;
@@ -23,7 +23,6 @@ interface TeamMemberItem {
23
23
  isCoordinator: boolean;
24
24
  hasToolRestrictions: boolean;
25
25
  toolProfile?: string;
26
- enabledSkills?: string[];
27
26
  modelTier?: ModelTier;
28
27
  modelId?: string;
29
28
  isMember: true;
@@ -67,7 +66,7 @@ interface TeamOverlayOptions {
67
66
  onAgentSwitch?: (agentId: string) => Promise<void>;
68
67
  }
69
68
  export interface TeamOverlayV2Result {
70
- action?: 'switch' | 'add' | 'remove' | 'compact' | 'create-custom' | 'change-tier';
69
+ action?: 'switch' | 'add' | 'remove' | 'compact' | 'create-custom' | 'change-tier' | 'edit';
71
70
  agentId?: string;
72
71
  role?: AgentRole;
73
72
  customId?: string;