@compilr-dev/cli 0.8.9 → 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.
- package/CHANGELOG.md +35 -0
- package/dist/agent.d.ts +9 -0
- package/dist/agent.js +1 -0
- package/dist/commands-v2/handlers/project.js +88 -229
- package/dist/commands-v2/handlers/skill.js +10 -6
- package/dist/commands-v2/handlers/team.d.ts +19 -1
- package/dist/commands-v2/handlers/team.js +178 -3
- package/dist/compilr-diff-companion.vsix +0 -0
- package/dist/index.js +55 -3
- package/dist/multi-agent/agent-edit.d.ts +75 -0
- package/dist/multi-agent/agent-edit.js +84 -0
- package/dist/multi-agent/capability-loader.d.ts +15 -5
- package/dist/multi-agent/capability-loader.js +18 -8
- package/dist/multi-agent/custom-agents.d.ts +12 -0
- package/dist/multi-agent/custom-agents.js +22 -1
- package/dist/repl-helpers.d.ts +6 -1
- package/dist/repl-helpers.js +24 -10
- package/dist/repl-v2.d.ts +10 -0
- package/dist/repl-v2.js +43 -2
- package/dist/ui/overlay/impl/agent-edit-overlay-v2.d.ts +105 -0
- package/dist/ui/overlay/impl/agent-edit-overlay-v2.js +364 -0
- package/dist/ui/overlay/impl/custom-agent-form-overlay-v2.d.ts +1 -8
- package/dist/ui/overlay/impl/custom-agent-form-overlay-v2.js +16 -120
- package/dist/ui/overlay/impl/project-edit-overlay-v2.d.ts +2 -1
- package/dist/ui/overlay/impl/project-edit-overlay-v2.js +26 -19
- package/dist/ui/overlay/impl/skills-overlay-v2.d.ts +42 -2
- package/dist/ui/overlay/impl/skills-overlay-v2.js +238 -185
- package/dist/ui/overlay/impl/team-overlay-v2.d.ts +1 -2
- package/dist/ui/overlay/impl/team-overlay-v2.js +6 -9
- package/dist/ui/terminal-types.d.ts +8 -0
- package/package.json +7 -3
|
@@ -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 {
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
}
|
|
125
|
-
}
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
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
|
-
|
|
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
|
-
|
|
213
|
+
macros;
|
|
214
|
+
phase = 'start';
|
|
215
|
+
route = 'blank';
|
|
217
216
|
nameInput = '';
|
|
218
217
|
selectedScope = 'project';
|
|
219
218
|
error = '';
|
|
220
219
|
hasProject;
|
|
221
|
-
|
|
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 (
|
|
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 (
|
|
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
|
-
|
|
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
|
-
|
|
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
|
|
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, ...
|
|
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(),
|
|
926
|
-
|
|
927
|
-
|
|
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
|
-
|
|
1013
|
-
|
|
1014
|
-
|
|
1015
|
-
|
|
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 = [...
|
|
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;
|