@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.
Files changed (84) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +308 -0
  3. package/bin/adg.ts +758 -0
  4. package/docs/agents-spec.md +132 -0
  5. package/docs/authoring.md +352 -0
  6. package/package.json +50 -0
  7. package/schemas/adg-plugin.schema.json +77 -0
  8. package/schemas/marketplace.schema.json +86 -0
  9. package/schemas/plugin-lock.schema.json +90 -0
  10. package/src/adapters/anthropic.ts +54 -0
  11. package/src/adapters/index.ts +24 -0
  12. package/src/adapters/openai.ts +37 -0
  13. package/src/adapters/reverse.ts +60 -0
  14. package/src/agents/claude.ts +124 -0
  15. package/src/agents/codex.ts +67 -0
  16. package/src/agents/index.ts +12 -0
  17. package/src/agents/registry.ts +30 -0
  18. package/src/agents/types.ts +47 -0
  19. package/src/commands/adapt.ts +36 -0
  20. package/src/commands/import.ts +69 -0
  21. package/src/commands/init.ts +146 -0
  22. package/src/commands/install.ts +411 -0
  23. package/src/commands/link.ts +61 -0
  24. package/src/commands/list.ts +28 -0
  25. package/src/commands/marketplace.ts +198 -0
  26. package/src/commands/migrate.ts +84 -0
  27. package/src/commands/multiselect-skills.ts +137 -0
  28. package/src/commands/remove.ts +136 -0
  29. package/src/commands/select-agents.ts +45 -0
  30. package/src/commands/select-components.ts +66 -0
  31. package/src/commands/select-plugins.ts +28 -0
  32. package/src/commands/select-scope.ts +21 -0
  33. package/src/commands/update.ts +85 -0
  34. package/src/commands/validate.ts +57 -0
  35. package/src/components.ts +90 -0
  36. package/src/deps.ts +64 -0
  37. package/src/fsutil.ts +38 -0
  38. package/src/hash.ts +61 -0
  39. package/src/lock.ts +57 -0
  40. package/src/manifest.ts +113 -0
  41. package/src/marketplace.ts +41 -0
  42. package/src/package.ts +74 -0
  43. package/src/paths.ts +129 -0
  44. package/src/semver.ts +67 -0
  45. package/src/skills.ts +88 -0
  46. package/src/sources.ts +159 -0
  47. package/src/types.ts +140 -0
  48. package/vendor/skills/LICENSE +29 -0
  49. package/vendor/skills/PROVENANCE.md +60 -0
  50. package/vendor/skills/ThirdPartyNoticeText.txt +117 -0
  51. package/vendor/skills/package.json +143 -0
  52. package/vendor/skills/src/add.ts +1999 -0
  53. package/vendor/skills/src/agents.ts +755 -0
  54. package/vendor/skills/src/blob.ts +567 -0
  55. package/vendor/skills/src/cli.ts +387 -0
  56. package/vendor/skills/src/constants.ts +3 -0
  57. package/vendor/skills/src/detect-agent.ts +62 -0
  58. package/vendor/skills/src/find.ts +357 -0
  59. package/vendor/skills/src/frontmatter.ts +16 -0
  60. package/vendor/skills/src/git-tree.ts +36 -0
  61. package/vendor/skills/src/git.ts +277 -0
  62. package/vendor/skills/src/install.ts +91 -0
  63. package/vendor/skills/src/installer.ts +1097 -0
  64. package/vendor/skills/src/list.ts +231 -0
  65. package/vendor/skills/src/local-lock.ts +182 -0
  66. package/vendor/skills/src/plugin-manifest.ts +183 -0
  67. package/vendor/skills/src/prompts/search-multiselect.ts +387 -0
  68. package/vendor/skills/src/providers/index.ts +14 -0
  69. package/vendor/skills/src/providers/registry.ts +51 -0
  70. package/vendor/skills/src/providers/types.ts +97 -0
  71. package/vendor/skills/src/providers/wellknown.ts +804 -0
  72. package/vendor/skills/src/remove.ts +323 -0
  73. package/vendor/skills/src/sanitize.ts +65 -0
  74. package/vendor/skills/src/self-cli.ts +20 -0
  75. package/vendor/skills/src/skill-lock.ts +329 -0
  76. package/vendor/skills/src/skills.ts +316 -0
  77. package/vendor/skills/src/source-parser.ts +438 -0
  78. package/vendor/skills/src/sync.ts +478 -0
  79. package/vendor/skills/src/telemetry.ts +186 -0
  80. package/vendor/skills/src/test-utils.ts +73 -0
  81. package/vendor/skills/src/types.ts +128 -0
  82. package/vendor/skills/src/update-source.ts +90 -0
  83. package/vendor/skills/src/update.ts +749 -0
  84. 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
+ }