@rbbtsn0w/adg 0.1.0-alpha.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.
- package/LICENSE +21 -0
- package/README.md +308 -0
- package/bin/adg.ts +758 -0
- package/docs/agents-spec.md +132 -0
- package/docs/authoring.md +352 -0
- package/package.json +50 -0
- package/schemas/adg-plugin.schema.json +77 -0
- package/schemas/marketplace.schema.json +86 -0
- package/schemas/plugin-lock.schema.json +90 -0
- package/src/adapters/anthropic.ts +54 -0
- package/src/adapters/index.ts +24 -0
- package/src/adapters/openai.ts +37 -0
- package/src/adapters/reverse.ts +60 -0
- package/src/agents/claude.ts +124 -0
- package/src/agents/codex.ts +67 -0
- package/src/agents/index.ts +12 -0
- package/src/agents/registry.ts +30 -0
- package/src/agents/types.ts +47 -0
- package/src/commands/adapt.ts +36 -0
- package/src/commands/import.ts +69 -0
- package/src/commands/init.ts +146 -0
- package/src/commands/install.ts +411 -0
- package/src/commands/link.ts +61 -0
- package/src/commands/list.ts +28 -0
- package/src/commands/marketplace.ts +198 -0
- package/src/commands/migrate.ts +84 -0
- package/src/commands/multiselect-skills.ts +137 -0
- package/src/commands/remove.ts +136 -0
- package/src/commands/select-agents.ts +45 -0
- package/src/commands/select-components.ts +66 -0
- package/src/commands/select-plugins.ts +28 -0
- package/src/commands/select-scope.ts +21 -0
- package/src/commands/update.ts +85 -0
- package/src/commands/validate.ts +57 -0
- package/src/components.ts +90 -0
- package/src/deps.ts +64 -0
- package/src/fsutil.ts +38 -0
- package/src/hash.ts +61 -0
- package/src/lock.ts +57 -0
- package/src/manifest.ts +113 -0
- package/src/marketplace.ts +41 -0
- package/src/package.ts +74 -0
- package/src/paths.ts +129 -0
- package/src/semver.ts +67 -0
- package/src/skills.ts +88 -0
- package/src/sources.ts +159 -0
- package/src/types.ts +140 -0
- package/vendor/skills/LICENSE +29 -0
- package/vendor/skills/PROVENANCE.md +60 -0
- package/vendor/skills/ThirdPartyNoticeText.txt +117 -0
- package/vendor/skills/package.json +143 -0
- package/vendor/skills/src/add.ts +1999 -0
- package/vendor/skills/src/agents.ts +755 -0
- package/vendor/skills/src/blob.ts +567 -0
- package/vendor/skills/src/cli.ts +387 -0
- package/vendor/skills/src/constants.ts +3 -0
- package/vendor/skills/src/detect-agent.ts +62 -0
- package/vendor/skills/src/find.ts +357 -0
- package/vendor/skills/src/frontmatter.ts +16 -0
- package/vendor/skills/src/git-tree.ts +36 -0
- package/vendor/skills/src/git.ts +277 -0
- package/vendor/skills/src/install.ts +91 -0
- package/vendor/skills/src/installer.ts +1097 -0
- package/vendor/skills/src/list.ts +231 -0
- package/vendor/skills/src/local-lock.ts +182 -0
- package/vendor/skills/src/plugin-manifest.ts +183 -0
- package/vendor/skills/src/prompts/search-multiselect.ts +387 -0
- package/vendor/skills/src/providers/index.ts +14 -0
- package/vendor/skills/src/providers/registry.ts +51 -0
- package/vendor/skills/src/providers/types.ts +97 -0
- package/vendor/skills/src/providers/wellknown.ts +804 -0
- package/vendor/skills/src/remove.ts +323 -0
- package/vendor/skills/src/sanitize.ts +65 -0
- package/vendor/skills/src/self-cli.ts +20 -0
- package/vendor/skills/src/skill-lock.ts +329 -0
- package/vendor/skills/src/skills.ts +316 -0
- package/vendor/skills/src/source-parser.ts +438 -0
- package/vendor/skills/src/sync.ts +478 -0
- package/vendor/skills/src/telemetry.ts +186 -0
- package/vendor/skills/src/test-utils.ts +73 -0
- package/vendor/skills/src/types.ts +128 -0
- package/vendor/skills/src/update-source.ts +90 -0
- package/vendor/skills/src/update.ts +749 -0
- package/vendor/skills/src/use.ts +675 -0
|
@@ -0,0 +1,387 @@
|
|
|
1
|
+
import * as readline from 'readline';
|
|
2
|
+
import { stripVTControlCharacters } from 'node:util';
|
|
3
|
+
import { Writable } from 'stream';
|
|
4
|
+
import pc from 'picocolors';
|
|
5
|
+
|
|
6
|
+
// Silent writable stream to prevent readline from echoing input
|
|
7
|
+
const silentOutput = new Writable({
|
|
8
|
+
write(_chunk, _encoding, callback) {
|
|
9
|
+
callback();
|
|
10
|
+
},
|
|
11
|
+
});
|
|
12
|
+
|
|
13
|
+
export interface SearchItem<T> {
|
|
14
|
+
value: T;
|
|
15
|
+
label: string;
|
|
16
|
+
hint?: string;
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
export interface LockedSection<T> {
|
|
20
|
+
title: string;
|
|
21
|
+
items: SearchItem<T>[];
|
|
22
|
+
hiddenCount?: number;
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
export interface SearchMultiselectOptions<T> {
|
|
26
|
+
message: string;
|
|
27
|
+
items: SearchItem<T>[];
|
|
28
|
+
maxVisible?: number;
|
|
29
|
+
initialSelected?: T[];
|
|
30
|
+
/** If true, require at least one item to be selected before submitting */
|
|
31
|
+
required?: boolean;
|
|
32
|
+
/** Locked section shown above the searchable list - items are always selected and can't be toggled */
|
|
33
|
+
lockedSection?: LockedSection<T>;
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
const S_STEP_ACTIVE = pc.green('◆');
|
|
37
|
+
const S_STEP_CANCEL = pc.red('■');
|
|
38
|
+
const S_STEP_SUBMIT = pc.green('◇');
|
|
39
|
+
const S_RADIO_ACTIVE = pc.green('●');
|
|
40
|
+
const S_RADIO_INACTIVE = pc.dim('○');
|
|
41
|
+
const S_CHECKBOX_LOCKED = pc.green('✓');
|
|
42
|
+
const S_BULLET = pc.green('•');
|
|
43
|
+
const S_BAR = pc.dim('│');
|
|
44
|
+
const S_BAR_H = pc.dim('─');
|
|
45
|
+
|
|
46
|
+
export const cancelSymbol = Symbol('cancel');
|
|
47
|
+
|
|
48
|
+
/**
|
|
49
|
+
* Approximate terminal display width (cells) for a string with no ANSI sequences.
|
|
50
|
+
* Matches common East Asian / emoji double-width behavior used by modern terminals.
|
|
51
|
+
*/
|
|
52
|
+
export function approxStringWidth(plain: string): number {
|
|
53
|
+
let width = 0;
|
|
54
|
+
for (const ch of plain) {
|
|
55
|
+
const code = ch.codePointAt(0)!;
|
|
56
|
+
if (code === 0) continue;
|
|
57
|
+
const wide =
|
|
58
|
+
(code >= 0x1100 && code <= 0x115f) ||
|
|
59
|
+
(code >= 0x231a && code <= 0x231b) ||
|
|
60
|
+
(code >= 0x2329 && code <= 0x232a) ||
|
|
61
|
+
(code >= 0x23e9 && code <= 0x23ec) ||
|
|
62
|
+
code === 0x23f0 ||
|
|
63
|
+
code === 0x23f3 ||
|
|
64
|
+
(code >= 0x25fd && code <= 0x25fe) ||
|
|
65
|
+
(code >= 0x2614 && code <= 0x2615) ||
|
|
66
|
+
(code >= 0x2648 && code <= 0x2653) ||
|
|
67
|
+
(code >= 0x267f && code <= 0x267f) ||
|
|
68
|
+
(code >= 0x2693 && code <= 0x2693) ||
|
|
69
|
+
(code >= 0x26a1 && code <= 0x26a1) ||
|
|
70
|
+
(code >= 0x26aa && code <= 0x26ab) ||
|
|
71
|
+
(code >= 0x26bd && code <= 0x26be) ||
|
|
72
|
+
(code >= 0x26c4 && code <= 0x26c5) ||
|
|
73
|
+
(code >= 0x26ce && code <= 0x26ce) ||
|
|
74
|
+
(code >= 0x26d4 && code <= 0x26d4) ||
|
|
75
|
+
(code >= 0x26ea && code <= 0x26ea) ||
|
|
76
|
+
(code >= 0x26f2 && code <= 0x26f3) ||
|
|
77
|
+
(code >= 0x26f5 && code <= 0x26f5) ||
|
|
78
|
+
(code >= 0x26fa && code <= 0x26fa) ||
|
|
79
|
+
(code >= 0x26fd && code <= 0x26fd) ||
|
|
80
|
+
(code >= 0x2705 && code <= 0x2705) ||
|
|
81
|
+
(code >= 0x270a && code <= 0x270b) ||
|
|
82
|
+
(code >= 0x2728 && code <= 0x2728) ||
|
|
83
|
+
(code >= 0x274c && code <= 0x274c) ||
|
|
84
|
+
(code >= 0x274e && code <= 0x274e) ||
|
|
85
|
+
(code >= 0x2753 && code <= 0x2755) ||
|
|
86
|
+
(code >= 0x2757 && code <= 0x2757) ||
|
|
87
|
+
(code >= 0x2795 && code <= 0x2797) ||
|
|
88
|
+
(code >= 0x27b0 && code <= 0x27b0) ||
|
|
89
|
+
(code >= 0x27bf && code <= 0x27bf) ||
|
|
90
|
+
(code >= 0x2b1b && code <= 0x2b1c) ||
|
|
91
|
+
(code >= 0x2b50 && code <= 0x2b50) ||
|
|
92
|
+
(code >= 0x2b55 && code <= 0x2b55) ||
|
|
93
|
+
(code >= 0x2e80 && code <= 0xa4cf && code !== 0x303f) ||
|
|
94
|
+
(code >= 0xa960 && code <= 0xa97c) ||
|
|
95
|
+
(code >= 0xac00 && code <= 0xd7a3) ||
|
|
96
|
+
(code >= 0xf900 && code <= 0xfaff) ||
|
|
97
|
+
(code >= 0xfe10 && code <= 0xfe19) ||
|
|
98
|
+
(code >= 0xfe30 && code <= 0xfe6f) ||
|
|
99
|
+
(code >= 0xff00 && code <= 0xff60) ||
|
|
100
|
+
(code >= 0xffe0 && code <= 0xffe6) ||
|
|
101
|
+
(code >= 0x1f000 && code <= 0x1f9ff);
|
|
102
|
+
width += wide ? 2 : 1;
|
|
103
|
+
}
|
|
104
|
+
return width;
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
/**
|
|
108
|
+
* How many physical terminal rows one logical line occupies after soft-wrapping.
|
|
109
|
+
*/
|
|
110
|
+
export function visualRowsForLine(line: string, columns: number): number {
|
|
111
|
+
const plain = stripVTControlCharacters(line);
|
|
112
|
+
const cols = Math.max(1, columns);
|
|
113
|
+
const w = approxStringWidth(plain);
|
|
114
|
+
return Math.max(1, Math.ceil(w / cols));
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
/**
|
|
118
|
+
* Total physical rows for a block of logical lines (used to erase/redraw TUI output).
|
|
119
|
+
*/
|
|
120
|
+
export function countVisualRowsForLines(lines: string[], columns: number | undefined): number {
|
|
121
|
+
const cols =
|
|
122
|
+
columns !== undefined && columns > 0
|
|
123
|
+
? columns
|
|
124
|
+
: process.stdout.columns && process.stdout.columns > 0
|
|
125
|
+
? process.stdout.columns
|
|
126
|
+
: 80;
|
|
127
|
+
return lines.reduce((sum, line) => sum + visualRowsForLine(line, cols), 0);
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
/**
|
|
131
|
+
* Interactive search multiselect prompt.
|
|
132
|
+
* Allows users to filter a long list by typing and select multiple items.
|
|
133
|
+
* Optionally supports a "locked" section that displays always-selected items.
|
|
134
|
+
*/
|
|
135
|
+
export async function searchMultiselect<T>(
|
|
136
|
+
options: SearchMultiselectOptions<T>
|
|
137
|
+
): Promise<T[] | symbol> {
|
|
138
|
+
const {
|
|
139
|
+
message,
|
|
140
|
+
items,
|
|
141
|
+
maxVisible = 8,
|
|
142
|
+
initialSelected = [],
|
|
143
|
+
required = false,
|
|
144
|
+
lockedSection,
|
|
145
|
+
} = options;
|
|
146
|
+
|
|
147
|
+
return new Promise((resolve) => {
|
|
148
|
+
const rl = readline.createInterface({
|
|
149
|
+
input: process.stdin,
|
|
150
|
+
output: silentOutput,
|
|
151
|
+
terminal: false,
|
|
152
|
+
});
|
|
153
|
+
|
|
154
|
+
// Enable raw mode for keypress detection
|
|
155
|
+
if (process.stdin.isTTY) {
|
|
156
|
+
process.stdin.setRawMode(true);
|
|
157
|
+
}
|
|
158
|
+
readline.emitKeypressEvents(process.stdin, rl);
|
|
159
|
+
|
|
160
|
+
let query = '';
|
|
161
|
+
let cursor = 0;
|
|
162
|
+
const selected = new Set<T>(initialSelected);
|
|
163
|
+
let lastRenderHeight = 0;
|
|
164
|
+
|
|
165
|
+
// Locked items are always included in the result
|
|
166
|
+
const lockedValues = lockedSection ? lockedSection.items.map((i) => i.value) : [];
|
|
167
|
+
|
|
168
|
+
const filter = (item: SearchItem<T>, q: string): boolean => {
|
|
169
|
+
if (!q) return true;
|
|
170
|
+
const lowerQ = q.toLowerCase();
|
|
171
|
+
return (
|
|
172
|
+
item.label.toLowerCase().includes(lowerQ) ||
|
|
173
|
+
String(item.value).toLowerCase().includes(lowerQ)
|
|
174
|
+
);
|
|
175
|
+
};
|
|
176
|
+
|
|
177
|
+
const getFiltered = (): SearchItem<T>[] => {
|
|
178
|
+
return items.filter((item) => filter(item, query));
|
|
179
|
+
};
|
|
180
|
+
|
|
181
|
+
const clearRender = (): void => {
|
|
182
|
+
if (lastRenderHeight > 0) {
|
|
183
|
+
// Move up and clear each line
|
|
184
|
+
process.stdout.write(`\x1b[${lastRenderHeight}A`);
|
|
185
|
+
for (let i = 0; i < lastRenderHeight; i++) {
|
|
186
|
+
process.stdout.write('\x1b[2K\x1b[1B');
|
|
187
|
+
}
|
|
188
|
+
process.stdout.write(`\x1b[${lastRenderHeight}A`);
|
|
189
|
+
}
|
|
190
|
+
};
|
|
191
|
+
|
|
192
|
+
const render = (state: 'active' | 'submit' | 'cancel' = 'active'): void => {
|
|
193
|
+
clearRender();
|
|
194
|
+
|
|
195
|
+
const lines: string[] = [];
|
|
196
|
+
const filtered = getFiltered();
|
|
197
|
+
|
|
198
|
+
// Header
|
|
199
|
+
const icon =
|
|
200
|
+
state === 'active' ? S_STEP_ACTIVE : state === 'cancel' ? S_STEP_CANCEL : S_STEP_SUBMIT;
|
|
201
|
+
lines.push(`${icon} ${pc.bold(message)}`);
|
|
202
|
+
|
|
203
|
+
if (state === 'active') {
|
|
204
|
+
// Locked section (universal agents)
|
|
205
|
+
if (lockedSection && lockedSection.items.length > 0) {
|
|
206
|
+
lines.push(`${S_BAR}`);
|
|
207
|
+
const lockedTitle = `${pc.bold(lockedSection.title)} ${pc.dim('── always included')}`;
|
|
208
|
+
lines.push(`${S_BAR} ${S_BAR_H}${S_BAR_H} ${lockedTitle} ${S_BAR_H.repeat(12)}`);
|
|
209
|
+
for (const item of lockedSection.items) {
|
|
210
|
+
lines.push(`${S_BAR} ${S_BULLET} ${pc.bold(item.label)}`);
|
|
211
|
+
}
|
|
212
|
+
if (lockedSection.hiddenCount && lockedSection.hiddenCount > 0) {
|
|
213
|
+
lines.push(`${S_BAR} ${pc.dim(`...and ${lockedSection.hiddenCount} more`)}`);
|
|
214
|
+
}
|
|
215
|
+
lines.push(`${S_BAR}`);
|
|
216
|
+
lines.push(
|
|
217
|
+
`${S_BAR} ${S_BAR_H}${S_BAR_H} ${pc.bold('Additional agents')} ${S_BAR_H.repeat(29)}`
|
|
218
|
+
);
|
|
219
|
+
}
|
|
220
|
+
|
|
221
|
+
// Search input
|
|
222
|
+
const searchLine = `${S_BAR} ${pc.dim('Search:')} ${query}${pc.inverse(' ')}`;
|
|
223
|
+
lines.push(searchLine);
|
|
224
|
+
|
|
225
|
+
// Hint
|
|
226
|
+
lines.push(`${S_BAR} ${pc.dim('↑↓ move, space select, enter confirm')}`);
|
|
227
|
+
lines.push(`${S_BAR}`);
|
|
228
|
+
|
|
229
|
+
// Items
|
|
230
|
+
const visibleStart = Math.max(
|
|
231
|
+
0,
|
|
232
|
+
Math.min(cursor - Math.floor(maxVisible / 2), filtered.length - maxVisible)
|
|
233
|
+
);
|
|
234
|
+
const visibleEnd = Math.min(filtered.length, visibleStart + maxVisible);
|
|
235
|
+
const visibleItems = filtered.slice(visibleStart, visibleEnd);
|
|
236
|
+
|
|
237
|
+
if (filtered.length === 0) {
|
|
238
|
+
lines.push(`${S_BAR} ${pc.dim('No matches found')}`);
|
|
239
|
+
} else {
|
|
240
|
+
for (let i = 0; i < visibleItems.length; i++) {
|
|
241
|
+
const item = visibleItems[i]!;
|
|
242
|
+
const actualIndex = visibleStart + i;
|
|
243
|
+
const isSelected = selected.has(item.value);
|
|
244
|
+
const isCursor = actualIndex === cursor;
|
|
245
|
+
|
|
246
|
+
const radio = isSelected ? S_RADIO_ACTIVE : S_RADIO_INACTIVE;
|
|
247
|
+
const label = isCursor ? pc.underline(item.label) : item.label;
|
|
248
|
+
const hint = item.hint ? pc.dim(` (${item.hint})`) : '';
|
|
249
|
+
|
|
250
|
+
const prefix = isCursor ? pc.cyan('❯') : ' ';
|
|
251
|
+
lines.push(`${S_BAR} ${prefix} ${radio} ${label}${hint}`);
|
|
252
|
+
}
|
|
253
|
+
|
|
254
|
+
// Show count if more items
|
|
255
|
+
const hiddenBefore = visibleStart;
|
|
256
|
+
const hiddenAfter = filtered.length - visibleEnd;
|
|
257
|
+
if (hiddenBefore > 0 || hiddenAfter > 0) {
|
|
258
|
+
const parts: string[] = [];
|
|
259
|
+
if (hiddenBefore > 0) parts.push(`↑ ${hiddenBefore} more`);
|
|
260
|
+
if (hiddenAfter > 0) parts.push(`↓ ${hiddenAfter} more`);
|
|
261
|
+
lines.push(`${S_BAR} ${pc.dim(parts.join(' '))}`);
|
|
262
|
+
}
|
|
263
|
+
}
|
|
264
|
+
|
|
265
|
+
// Selected summary (include locked items)
|
|
266
|
+
lines.push(`${S_BAR}`);
|
|
267
|
+
const allSelectedLabels = [
|
|
268
|
+
...(lockedSection ? lockedSection.items.map((i) => i.label) : []),
|
|
269
|
+
...items.filter((item) => selected.has(item.value)).map((item) => item.label),
|
|
270
|
+
];
|
|
271
|
+
if (allSelectedLabels.length === 0) {
|
|
272
|
+
lines.push(`${S_BAR} ${pc.dim('Selected: (none)')}`);
|
|
273
|
+
} else {
|
|
274
|
+
const summary =
|
|
275
|
+
allSelectedLabels.length <= 3
|
|
276
|
+
? allSelectedLabels.join(', ')
|
|
277
|
+
: `${allSelectedLabels.slice(0, 3).join(', ')} +${allSelectedLabels.length - 3} more`;
|
|
278
|
+
lines.push(`${S_BAR} ${pc.green('Selected:')} ${summary}`);
|
|
279
|
+
}
|
|
280
|
+
|
|
281
|
+
lines.push(`${pc.dim('└')}`);
|
|
282
|
+
} else if (state === 'submit') {
|
|
283
|
+
// Final state - show what was selected (including locked)
|
|
284
|
+
const allSelectedLabels = [
|
|
285
|
+
...(lockedSection ? lockedSection.items.map((i) => i.label) : []),
|
|
286
|
+
...items.filter((item) => selected.has(item.value)).map((item) => item.label),
|
|
287
|
+
];
|
|
288
|
+
lines.push(`${S_BAR} ${pc.dim(allSelectedLabels.join(', '))}`);
|
|
289
|
+
} else if (state === 'cancel') {
|
|
290
|
+
lines.push(`${S_BAR} ${pc.strikethrough(pc.dim('Cancelled'))}`);
|
|
291
|
+
}
|
|
292
|
+
|
|
293
|
+
process.stdout.write(lines.join('\n') + '\n');
|
|
294
|
+
// Use wrapped row count: logical lines can span multiple terminal rows when hints
|
|
295
|
+
// or labels exceed column width. Using lines.length alone under-counts and breaks
|
|
296
|
+
// clearRender(), causing the prompt to re-print hundreds of times on each redraw.
|
|
297
|
+
lastRenderHeight = countVisualRowsForLines(lines, process.stdout.columns);
|
|
298
|
+
};
|
|
299
|
+
|
|
300
|
+
const cleanup = (): void => {
|
|
301
|
+
process.stdin.removeListener('keypress', keypressHandler);
|
|
302
|
+
if (process.stdin.isTTY) {
|
|
303
|
+
process.stdin.setRawMode(false);
|
|
304
|
+
}
|
|
305
|
+
rl.close();
|
|
306
|
+
};
|
|
307
|
+
|
|
308
|
+
const submit = (): void => {
|
|
309
|
+
// If required and no locked items, don't allow submitting with no selection
|
|
310
|
+
if (required && selected.size === 0 && lockedValues.length === 0) {
|
|
311
|
+
return;
|
|
312
|
+
}
|
|
313
|
+
render('submit');
|
|
314
|
+
cleanup();
|
|
315
|
+
// Include locked values in the result
|
|
316
|
+
resolve([...lockedValues, ...Array.from(selected)]);
|
|
317
|
+
};
|
|
318
|
+
|
|
319
|
+
const cancel = (): void => {
|
|
320
|
+
render('cancel');
|
|
321
|
+
cleanup();
|
|
322
|
+
resolve(cancelSymbol);
|
|
323
|
+
};
|
|
324
|
+
|
|
325
|
+
// Handle keypresses
|
|
326
|
+
const keypressHandler = (_str: string, key: readline.Key): void => {
|
|
327
|
+
if (!key) return;
|
|
328
|
+
|
|
329
|
+
const filtered = getFiltered();
|
|
330
|
+
|
|
331
|
+
if (key.name === 'return') {
|
|
332
|
+
submit();
|
|
333
|
+
return;
|
|
334
|
+
}
|
|
335
|
+
|
|
336
|
+
if (key.name === 'escape' || (key.ctrl && key.name === 'c')) {
|
|
337
|
+
cancel();
|
|
338
|
+
return;
|
|
339
|
+
}
|
|
340
|
+
|
|
341
|
+
if (key.name === 'up') {
|
|
342
|
+
cursor = Math.max(0, cursor - 1);
|
|
343
|
+
render();
|
|
344
|
+
return;
|
|
345
|
+
}
|
|
346
|
+
|
|
347
|
+
if (key.name === 'down') {
|
|
348
|
+
cursor = Math.min(filtered.length - 1, cursor + 1);
|
|
349
|
+
render();
|
|
350
|
+
return;
|
|
351
|
+
}
|
|
352
|
+
|
|
353
|
+
if (key.name === 'space') {
|
|
354
|
+
const item = filtered[cursor];
|
|
355
|
+
if (item) {
|
|
356
|
+
if (selected.has(item.value)) {
|
|
357
|
+
selected.delete(item.value);
|
|
358
|
+
} else {
|
|
359
|
+
selected.add(item.value);
|
|
360
|
+
}
|
|
361
|
+
}
|
|
362
|
+
render();
|
|
363
|
+
return;
|
|
364
|
+
}
|
|
365
|
+
|
|
366
|
+
if (key.name === 'backspace') {
|
|
367
|
+
query = query.slice(0, -1);
|
|
368
|
+
cursor = 0;
|
|
369
|
+
render();
|
|
370
|
+
return;
|
|
371
|
+
}
|
|
372
|
+
|
|
373
|
+
// Regular character input
|
|
374
|
+
if (key.sequence && !key.ctrl && !key.meta && key.sequence.length === 1) {
|
|
375
|
+
query += key.sequence;
|
|
376
|
+
cursor = 0;
|
|
377
|
+
render();
|
|
378
|
+
return;
|
|
379
|
+
}
|
|
380
|
+
};
|
|
381
|
+
|
|
382
|
+
process.stdin.on('keypress', keypressHandler);
|
|
383
|
+
|
|
384
|
+
// Initial render
|
|
385
|
+
render();
|
|
386
|
+
});
|
|
387
|
+
}
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
// Export types
|
|
2
|
+
export type { HostProvider, ProviderMatch, ProviderRegistry, RemoteSkill } from './types.ts';
|
|
3
|
+
|
|
4
|
+
// Export registry functions
|
|
5
|
+
export { registry, registerProvider, findProvider, getProviders } from './registry.ts';
|
|
6
|
+
|
|
7
|
+
// Export individual providers
|
|
8
|
+
export {
|
|
9
|
+
WellKnownProvider,
|
|
10
|
+
wellKnownProvider,
|
|
11
|
+
type WellKnownIndex,
|
|
12
|
+
type WellKnownSkillEntry,
|
|
13
|
+
type WellKnownSkill,
|
|
14
|
+
} from './wellknown.ts';
|
|
@@ -0,0 +1,51 @@
|
|
|
1
|
+
import type { HostProvider, ProviderRegistry } from './types.ts';
|
|
2
|
+
|
|
3
|
+
class ProviderRegistryImpl implements ProviderRegistry {
|
|
4
|
+
private providers: HostProvider[] = [];
|
|
5
|
+
|
|
6
|
+
register(provider: HostProvider): void {
|
|
7
|
+
// Check for duplicate IDs
|
|
8
|
+
if (this.providers.some((p) => p.id === provider.id)) {
|
|
9
|
+
throw new Error(`Provider with id "${provider.id}" already registered`);
|
|
10
|
+
}
|
|
11
|
+
this.providers.push(provider);
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
findProvider(url: string): HostProvider | null {
|
|
15
|
+
for (const provider of this.providers) {
|
|
16
|
+
const match = provider.match(url);
|
|
17
|
+
if (match.matches) {
|
|
18
|
+
return provider;
|
|
19
|
+
}
|
|
20
|
+
}
|
|
21
|
+
return null;
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
getProviders(): HostProvider[] {
|
|
25
|
+
return [...this.providers];
|
|
26
|
+
}
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
// Singleton registry instance
|
|
30
|
+
export const registry = new ProviderRegistryImpl();
|
|
31
|
+
|
|
32
|
+
/**
|
|
33
|
+
* Register a provider with the global registry.
|
|
34
|
+
*/
|
|
35
|
+
export function registerProvider(provider: HostProvider): void {
|
|
36
|
+
registry.register(provider);
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
/**
|
|
40
|
+
* Find a provider that matches the given URL.
|
|
41
|
+
*/
|
|
42
|
+
export function findProvider(url: string): HostProvider | null {
|
|
43
|
+
return registry.findProvider(url);
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
/**
|
|
47
|
+
* Get all registered providers.
|
|
48
|
+
*/
|
|
49
|
+
export function getProviders(): HostProvider[] {
|
|
50
|
+
return registry.getProviders();
|
|
51
|
+
}
|
|
@@ -0,0 +1,97 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Represents a parsed skill from a remote host.
|
|
3
|
+
* Different hosts may have different ways of identifying skills.
|
|
4
|
+
*/
|
|
5
|
+
export interface RemoteSkill {
|
|
6
|
+
/** Display name of the skill (from frontmatter) */
|
|
7
|
+
name: string;
|
|
8
|
+
/** Description of the skill (from frontmatter) */
|
|
9
|
+
description: string;
|
|
10
|
+
/** Full markdown content including frontmatter */
|
|
11
|
+
content: string;
|
|
12
|
+
/** The identifier used for installation directory name */
|
|
13
|
+
installName: string;
|
|
14
|
+
/** The original source URL */
|
|
15
|
+
sourceUrl: string;
|
|
16
|
+
/** Any additional metadata from frontmatter */
|
|
17
|
+
metadata?: Record<string, unknown>;
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
/**
|
|
21
|
+
* Result of attempting to match a URL to a provider.
|
|
22
|
+
*/
|
|
23
|
+
export interface ProviderMatch {
|
|
24
|
+
/** Whether the URL matches this provider */
|
|
25
|
+
matches: boolean;
|
|
26
|
+
/** The source identifier for telemetry/storage (e.g., "mintlify/bun.com", "huggingface/hf-skills/hf-jobs") */
|
|
27
|
+
sourceIdentifier?: string;
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
/**
|
|
31
|
+
* Interface for remote SKILL.md host providers.
|
|
32
|
+
* Each provider knows how to:
|
|
33
|
+
* - Detect if a URL belongs to it
|
|
34
|
+
* - Fetch and parse SKILL.md files
|
|
35
|
+
* - Convert URLs to raw content URLs
|
|
36
|
+
* - Provide source identifiers for telemetry
|
|
37
|
+
*/
|
|
38
|
+
export interface HostProvider {
|
|
39
|
+
/** Unique identifier for this provider (e.g., "mintlify", "huggingface", "github") */
|
|
40
|
+
readonly id: string;
|
|
41
|
+
|
|
42
|
+
/** Display name for this provider */
|
|
43
|
+
readonly displayName: string;
|
|
44
|
+
|
|
45
|
+
/**
|
|
46
|
+
* Check if a URL matches this provider.
|
|
47
|
+
* @param url - The URL to check
|
|
48
|
+
* @returns Match result with optional source identifier
|
|
49
|
+
*/
|
|
50
|
+
match(url: string): ProviderMatch;
|
|
51
|
+
|
|
52
|
+
/**
|
|
53
|
+
* Fetch and parse a SKILL.md file from the given URL.
|
|
54
|
+
* @param url - The URL to the SKILL.md file
|
|
55
|
+
* @returns The parsed skill or null if invalid/not found
|
|
56
|
+
*/
|
|
57
|
+
fetchSkill(url: string): Promise<RemoteSkill | null>;
|
|
58
|
+
|
|
59
|
+
/**
|
|
60
|
+
* Convert a user-facing URL to a raw content URL.
|
|
61
|
+
* For example, GitHub blob URLs to raw.githubusercontent.com URLs.
|
|
62
|
+
* @param url - The URL to convert
|
|
63
|
+
* @returns The raw content URL
|
|
64
|
+
*/
|
|
65
|
+
toRawUrl(url: string): string;
|
|
66
|
+
|
|
67
|
+
/**
|
|
68
|
+
* Get the source identifier for telemetry/storage.
|
|
69
|
+
* This should be a stable identifier that can be used to group
|
|
70
|
+
* skills from the same source.
|
|
71
|
+
* @param url - The original URL
|
|
72
|
+
* @returns Source identifier (e.g., "mintlify/bun.com", "huggingface/hf-skills/hf-jobs")
|
|
73
|
+
*/
|
|
74
|
+
getSourceIdentifier(url: string): string;
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
/**
|
|
78
|
+
* Registry for managing host providers.
|
|
79
|
+
*/
|
|
80
|
+
export interface ProviderRegistry {
|
|
81
|
+
/**
|
|
82
|
+
* Register a new provider.
|
|
83
|
+
*/
|
|
84
|
+
register(provider: HostProvider): void;
|
|
85
|
+
|
|
86
|
+
/**
|
|
87
|
+
* Find a provider that matches the given URL.
|
|
88
|
+
* @param url - The URL to match
|
|
89
|
+
* @returns The matching provider or null
|
|
90
|
+
*/
|
|
91
|
+
findProvider(url: string): HostProvider | null;
|
|
92
|
+
|
|
93
|
+
/**
|
|
94
|
+
* Get all registered providers.
|
|
95
|
+
*/
|
|
96
|
+
getProviders(): HostProvider[];
|
|
97
|
+
}
|