@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,438 @@
|
|
|
1
|
+
import { isAbsolute, resolve } from 'path';
|
|
2
|
+
import type { ParsedSource } from './types.ts';
|
|
3
|
+
|
|
4
|
+
/**
|
|
5
|
+
* Extract owner/repo (or group/subgroup/repo for GitLab) from a parsed source
|
|
6
|
+
* for lockfile tracking and telemetry.
|
|
7
|
+
* Returns null for local paths or unparseable sources.
|
|
8
|
+
* Supports any Git host with an owner/repo URL structure, including GitLab subgroups.
|
|
9
|
+
*/
|
|
10
|
+
export function getOwnerRepo(parsed: ParsedSource): string | null {
|
|
11
|
+
if (parsed.type === 'local') {
|
|
12
|
+
return null;
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
// Handle Git SSH URLs (e.g., git@gitlab.com:owner/repo.git, git@github.com:owner/repo.git)
|
|
16
|
+
const sshMatch = parsed.url.match(/^git@[^:]+:(.+)$/);
|
|
17
|
+
if (sshMatch) {
|
|
18
|
+
let path = sshMatch[1]!;
|
|
19
|
+
path = path.replace(/\.git$/, '');
|
|
20
|
+
|
|
21
|
+
// Must have at least owner/repo (one slash)
|
|
22
|
+
if (path.includes('/')) {
|
|
23
|
+
return path;
|
|
24
|
+
}
|
|
25
|
+
return null;
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
// Handle SSH URLs with a scheme (e.g., ssh://git@host:7999/owner/repo.git)
|
|
29
|
+
if (parsed.url.startsWith('ssh://')) {
|
|
30
|
+
try {
|
|
31
|
+
const url = new URL(parsed.url);
|
|
32
|
+
let path = url.pathname.slice(1);
|
|
33
|
+
path = path.replace(/\.git$/, '');
|
|
34
|
+
|
|
35
|
+
if (path.includes('/')) {
|
|
36
|
+
return path;
|
|
37
|
+
}
|
|
38
|
+
return null;
|
|
39
|
+
} catch {
|
|
40
|
+
return null;
|
|
41
|
+
}
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
// Handle HTTP(S) URLs
|
|
45
|
+
if (!parsed.url.startsWith('http://') && !parsed.url.startsWith('https://')) {
|
|
46
|
+
return null;
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
try {
|
|
50
|
+
const url = new URL(parsed.url);
|
|
51
|
+
// Get pathname, remove leading slash and trailing .git
|
|
52
|
+
let path = url.pathname.slice(1);
|
|
53
|
+
path = path.replace(/\.git$/, '');
|
|
54
|
+
|
|
55
|
+
// Must have at least owner/repo (one slash)
|
|
56
|
+
if (path.includes('/')) {
|
|
57
|
+
return path;
|
|
58
|
+
}
|
|
59
|
+
} catch {
|
|
60
|
+
// Invalid URL
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
return null;
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
/**
|
|
67
|
+
* Extract owner and repo from an owner/repo string.
|
|
68
|
+
* Returns null if the format is invalid.
|
|
69
|
+
*/
|
|
70
|
+
export function parseOwnerRepo(ownerRepo: string): { owner: string; repo: string } | null {
|
|
71
|
+
const match = ownerRepo.match(/^([^/]+)\/([^/]+)$/);
|
|
72
|
+
if (match) {
|
|
73
|
+
return { owner: match[1]!, repo: match[2]! };
|
|
74
|
+
}
|
|
75
|
+
return null;
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
/**
|
|
79
|
+
* Check if a GitHub repository is private.
|
|
80
|
+
* Returns true if private, false if public, null if unable to determine.
|
|
81
|
+
* Only works for GitHub repositories (GitLab not supported).
|
|
82
|
+
*/
|
|
83
|
+
export async function isRepoPrivate(owner: string, repo: string): Promise<boolean | null> {
|
|
84
|
+
try {
|
|
85
|
+
const res = await fetch(`https://api.github.com/repos/${owner}/${repo}`);
|
|
86
|
+
|
|
87
|
+
// If repo doesn't exist or we don't have access, assume private to be safe
|
|
88
|
+
if (!res.ok) {
|
|
89
|
+
return null; // Unable to determine
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
const data = (await res.json()) as { private?: boolean };
|
|
93
|
+
return data.private === true;
|
|
94
|
+
} catch {
|
|
95
|
+
// On error, return null to indicate we couldn't determine
|
|
96
|
+
return null;
|
|
97
|
+
}
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
/**
|
|
101
|
+
* Sanitizes a subpath to prevent path traversal attacks.
|
|
102
|
+
* Rejects subpaths containing ".." segments that could escape the repository root.
|
|
103
|
+
* Returns the sanitized subpath, or throws if the subpath is unsafe.
|
|
104
|
+
*/
|
|
105
|
+
export function sanitizeSubpath(subpath: string): string {
|
|
106
|
+
// Normalize to forward slashes for consistent handling
|
|
107
|
+
const normalized = subpath.replace(/\\/g, '/');
|
|
108
|
+
|
|
109
|
+
// Check each segment for ".."
|
|
110
|
+
const segments = normalized.split('/');
|
|
111
|
+
for (const segment of segments) {
|
|
112
|
+
if (segment === '..') {
|
|
113
|
+
throw new Error(
|
|
114
|
+
`Unsafe subpath: "${subpath}" contains path traversal segments. ` +
|
|
115
|
+
`Subpaths must not contain ".." components.`
|
|
116
|
+
);
|
|
117
|
+
}
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
return subpath;
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
/**
|
|
124
|
+
* Check if a string represents a local file system path
|
|
125
|
+
*/
|
|
126
|
+
function isLocalPath(input: string): boolean {
|
|
127
|
+
return (
|
|
128
|
+
isAbsolute(input) ||
|
|
129
|
+
input.startsWith('./') ||
|
|
130
|
+
input.startsWith('../') ||
|
|
131
|
+
input === '.' ||
|
|
132
|
+
input === '..' ||
|
|
133
|
+
// Windows absolute paths like C:\ or D:\
|
|
134
|
+
/^[a-zA-Z]:[/\\]/.test(input)
|
|
135
|
+
);
|
|
136
|
+
}
|
|
137
|
+
|
|
138
|
+
/**
|
|
139
|
+
* Parse a source string into a structured format
|
|
140
|
+
* Supports: local paths, GitHub URLs, GitLab URLs, GitHub shorthand, well-known URLs, and direct git URLs
|
|
141
|
+
*/
|
|
142
|
+
// Source aliases: map common shorthand to canonical source
|
|
143
|
+
const SOURCE_ALIASES: Record<string, string> = {
|
|
144
|
+
'coinbase/agentWallet': 'coinbase/agentic-wallet-skills',
|
|
145
|
+
};
|
|
146
|
+
|
|
147
|
+
interface FragmentRefResult {
|
|
148
|
+
inputWithoutFragment: string;
|
|
149
|
+
ref?: string;
|
|
150
|
+
skillFilter?: string;
|
|
151
|
+
}
|
|
152
|
+
|
|
153
|
+
function decodeFragmentValue(value: string): string {
|
|
154
|
+
try {
|
|
155
|
+
return decodeURIComponent(value);
|
|
156
|
+
} catch {
|
|
157
|
+
return value;
|
|
158
|
+
}
|
|
159
|
+
}
|
|
160
|
+
|
|
161
|
+
function looksLikeGitSource(input: string): boolean {
|
|
162
|
+
if (input.startsWith('github:') || input.startsWith('gitlab:') || input.startsWith('git@')) {
|
|
163
|
+
return true;
|
|
164
|
+
}
|
|
165
|
+
|
|
166
|
+
if (/^ssh:\/\/.+\.git(?:$|[/?])/i.test(input)) {
|
|
167
|
+
return true;
|
|
168
|
+
}
|
|
169
|
+
|
|
170
|
+
if (input.startsWith('http://') || input.startsWith('https://')) {
|
|
171
|
+
try {
|
|
172
|
+
const parsed = new URL(input);
|
|
173
|
+
const pathname = parsed.pathname;
|
|
174
|
+
|
|
175
|
+
// Only treat GitHub fragments as refs for repo/tree URLs.
|
|
176
|
+
if (parsed.hostname === 'github.com') {
|
|
177
|
+
return /^\/[^/]+\/[^/]+(?:\.git)?(?:\/tree\/[^/]+(?:\/.*)?)?\/?$/.test(pathname);
|
|
178
|
+
}
|
|
179
|
+
|
|
180
|
+
// Only treat gitlab.com fragments as refs for repo/tree URLs.
|
|
181
|
+
if (parsed.hostname === 'gitlab.com') {
|
|
182
|
+
return /^\/.+?\/[^/]+(?:\.git)?(?:\/-\/tree\/[^/]+(?:\/.*)?)?\/?$/.test(pathname);
|
|
183
|
+
}
|
|
184
|
+
} catch {
|
|
185
|
+
// Fall through to generic checks below.
|
|
186
|
+
}
|
|
187
|
+
}
|
|
188
|
+
|
|
189
|
+
if (/^https?:\/\/.+\.git(?:$|[/?])/i.test(input)) {
|
|
190
|
+
return true;
|
|
191
|
+
}
|
|
192
|
+
|
|
193
|
+
return (
|
|
194
|
+
!input.includes(':') &&
|
|
195
|
+
!input.startsWith('.') &&
|
|
196
|
+
!input.startsWith('/') &&
|
|
197
|
+
/^([^/]+)\/([^/]+)(?:\/(.+)|@(.+))?$/.test(input)
|
|
198
|
+
);
|
|
199
|
+
}
|
|
200
|
+
|
|
201
|
+
function parseFragmentRef(input: string): FragmentRefResult {
|
|
202
|
+
const hashIndex = input.indexOf('#');
|
|
203
|
+
if (hashIndex < 0) {
|
|
204
|
+
return { inputWithoutFragment: input };
|
|
205
|
+
}
|
|
206
|
+
|
|
207
|
+
const inputWithoutFragment = input.slice(0, hashIndex);
|
|
208
|
+
const fragment = input.slice(hashIndex + 1);
|
|
209
|
+
|
|
210
|
+
// Treat URL fragments as git refs only for git-like sources.
|
|
211
|
+
// This avoids changing behavior for generic well-known URLs.
|
|
212
|
+
if (!fragment || !looksLikeGitSource(inputWithoutFragment)) {
|
|
213
|
+
return { inputWithoutFragment: input };
|
|
214
|
+
}
|
|
215
|
+
|
|
216
|
+
const atIndex = fragment.indexOf('@');
|
|
217
|
+
if (atIndex === -1) {
|
|
218
|
+
return {
|
|
219
|
+
inputWithoutFragment,
|
|
220
|
+
ref: decodeFragmentValue(fragment),
|
|
221
|
+
};
|
|
222
|
+
}
|
|
223
|
+
|
|
224
|
+
const ref = fragment.slice(0, atIndex);
|
|
225
|
+
const skillFilter = fragment.slice(atIndex + 1);
|
|
226
|
+
return {
|
|
227
|
+
inputWithoutFragment,
|
|
228
|
+
ref: ref ? decodeFragmentValue(ref) : undefined,
|
|
229
|
+
skillFilter: skillFilter ? decodeFragmentValue(skillFilter) : undefined,
|
|
230
|
+
};
|
|
231
|
+
}
|
|
232
|
+
|
|
233
|
+
function appendFragmentRef(input: string, ref?: string, skillFilter?: string): string {
|
|
234
|
+
if (!ref) {
|
|
235
|
+
return input;
|
|
236
|
+
}
|
|
237
|
+
return `${input}#${ref}${skillFilter ? `@${skillFilter}` : ''}`;
|
|
238
|
+
}
|
|
239
|
+
|
|
240
|
+
export function parseSource(input: string): ParsedSource {
|
|
241
|
+
// Local path: absolute, relative, or current directory
|
|
242
|
+
if (isLocalPath(input)) {
|
|
243
|
+
const resolvedPath = resolve(input);
|
|
244
|
+
// Return local type even if path doesn't exist - we'll handle validation in main flow
|
|
245
|
+
return {
|
|
246
|
+
type: 'local',
|
|
247
|
+
url: resolvedPath, // Store resolved path in url for consistency
|
|
248
|
+
localPath: resolvedPath,
|
|
249
|
+
};
|
|
250
|
+
}
|
|
251
|
+
|
|
252
|
+
const {
|
|
253
|
+
inputWithoutFragment,
|
|
254
|
+
ref: fragmentRef,
|
|
255
|
+
skillFilter: fragmentSkillFilter,
|
|
256
|
+
} = parseFragmentRef(input);
|
|
257
|
+
input = inputWithoutFragment;
|
|
258
|
+
|
|
259
|
+
// Resolve source aliases before parsing
|
|
260
|
+
const alias = SOURCE_ALIASES[input];
|
|
261
|
+
if (alias) {
|
|
262
|
+
input = alias;
|
|
263
|
+
}
|
|
264
|
+
|
|
265
|
+
// Prefix shorthand: github:owner/repo -> owner/repo (handled by existing shorthand logic)
|
|
266
|
+
// Also supports github:owner/repo/subpath and github:owner/repo@skill
|
|
267
|
+
const githubPrefixMatch = input.match(/^github:(.+)$/);
|
|
268
|
+
if (githubPrefixMatch) {
|
|
269
|
+
return parseSource(appendFragmentRef(githubPrefixMatch[1]!, fragmentRef, fragmentSkillFilter));
|
|
270
|
+
}
|
|
271
|
+
|
|
272
|
+
// Prefix shorthand: gitlab:owner/repo -> https://gitlab.com/owner/repo
|
|
273
|
+
const gitlabPrefixMatch = input.match(/^gitlab:(.+)$/);
|
|
274
|
+
if (gitlabPrefixMatch) {
|
|
275
|
+
return parseSource(
|
|
276
|
+
appendFragmentRef(
|
|
277
|
+
`https://gitlab.com/${gitlabPrefixMatch[1]!}`,
|
|
278
|
+
fragmentRef,
|
|
279
|
+
fragmentSkillFilter
|
|
280
|
+
)
|
|
281
|
+
);
|
|
282
|
+
}
|
|
283
|
+
|
|
284
|
+
// GitHub URL with path: https://github.com/owner/repo/tree/branch/path/to/skill
|
|
285
|
+
const githubTreeWithPathMatch = input.match(/github\.com\/([^/]+)\/([^/]+)\/tree\/([^/]+)\/(.+)/);
|
|
286
|
+
if (githubTreeWithPathMatch) {
|
|
287
|
+
const [, owner, repo, ref, subpath] = githubTreeWithPathMatch;
|
|
288
|
+
return {
|
|
289
|
+
type: 'github',
|
|
290
|
+
url: `https://github.com/${owner}/${repo}.git`,
|
|
291
|
+
ref: ref || fragmentRef,
|
|
292
|
+
subpath: subpath ? sanitizeSubpath(subpath) : subpath,
|
|
293
|
+
};
|
|
294
|
+
}
|
|
295
|
+
|
|
296
|
+
// GitHub URL with branch only: https://github.com/owner/repo/tree/branch
|
|
297
|
+
const githubTreeMatch = input.match(/github\.com\/([^/]+)\/([^/]+)\/tree\/([^/]+)$/);
|
|
298
|
+
if (githubTreeMatch) {
|
|
299
|
+
const [, owner, repo, ref] = githubTreeMatch;
|
|
300
|
+
return {
|
|
301
|
+
type: 'github',
|
|
302
|
+
url: `https://github.com/${owner}/${repo}.git`,
|
|
303
|
+
ref: ref || fragmentRef,
|
|
304
|
+
};
|
|
305
|
+
}
|
|
306
|
+
|
|
307
|
+
// GitHub URL: https://github.com/owner/repo
|
|
308
|
+
const githubRepoMatch = input.match(/github\.com\/([^/]+)\/([^/]+)/);
|
|
309
|
+
if (githubRepoMatch) {
|
|
310
|
+
const [, owner, repo] = githubRepoMatch;
|
|
311
|
+
const cleanRepo = repo!.replace(/\.git$/, '');
|
|
312
|
+
return {
|
|
313
|
+
type: 'github',
|
|
314
|
+
url: `https://github.com/${owner}/${cleanRepo}.git`,
|
|
315
|
+
...(fragmentRef ? { ref: fragmentRef } : {}),
|
|
316
|
+
};
|
|
317
|
+
}
|
|
318
|
+
|
|
319
|
+
// GitLab URL with path (any GitLab instance): https://gitlab.com/owner/repo/-/tree/branch/path
|
|
320
|
+
// Key identifier is the "/-/tree/" path pattern unique to GitLab.
|
|
321
|
+
// Supports subgroups by using a non-greedy match for the repository path.
|
|
322
|
+
const gitlabTreeWithPathMatch = input.match(
|
|
323
|
+
/^(https?):\/\/([^/]+)\/(.+?)\/-\/tree\/([^/]+)\/(.+)/
|
|
324
|
+
);
|
|
325
|
+
if (gitlabTreeWithPathMatch) {
|
|
326
|
+
const [, protocol, hostname, repoPath, ref, subpath] = gitlabTreeWithPathMatch;
|
|
327
|
+
if (hostname !== 'github.com' && repoPath) {
|
|
328
|
+
return {
|
|
329
|
+
type: 'gitlab',
|
|
330
|
+
url: `${protocol}://${hostname}/${repoPath.replace(/\.git$/, '')}.git`,
|
|
331
|
+
ref: ref || fragmentRef,
|
|
332
|
+
subpath: subpath ? sanitizeSubpath(subpath) : subpath,
|
|
333
|
+
};
|
|
334
|
+
}
|
|
335
|
+
}
|
|
336
|
+
|
|
337
|
+
// GitLab URL with branch only (any GitLab instance): https://gitlab.com/owner/repo/-/tree/branch
|
|
338
|
+
const gitlabTreeMatch = input.match(/^(https?):\/\/([^/]+)\/(.+?)\/-\/tree\/([^/]+)$/);
|
|
339
|
+
if (gitlabTreeMatch) {
|
|
340
|
+
const [, protocol, hostname, repoPath, ref] = gitlabTreeMatch;
|
|
341
|
+
if (hostname !== 'github.com' && repoPath) {
|
|
342
|
+
return {
|
|
343
|
+
type: 'gitlab',
|
|
344
|
+
url: `${protocol}://${hostname}/${repoPath.replace(/\.git$/, '')}.git`,
|
|
345
|
+
ref: ref || fragmentRef,
|
|
346
|
+
};
|
|
347
|
+
}
|
|
348
|
+
}
|
|
349
|
+
|
|
350
|
+
// GitLab.com URL: https://gitlab.com/owner/repo or https://gitlab.com/group/subgroup/repo
|
|
351
|
+
// Only for the official gitlab.com domain for user convenience.
|
|
352
|
+
// Supports nested subgroups (e.g., gitlab.com/group/subgroup1/subgroup2/repo).
|
|
353
|
+
const gitlabRepoMatch = input.match(/gitlab\.com\/(.+?)(?:\.git)?\/?$/);
|
|
354
|
+
if (gitlabRepoMatch) {
|
|
355
|
+
const repoPath = gitlabRepoMatch[1]!;
|
|
356
|
+
// Must have at least owner/repo (one slash)
|
|
357
|
+
if (repoPath.includes('/')) {
|
|
358
|
+
return {
|
|
359
|
+
type: 'gitlab',
|
|
360
|
+
url: `https://gitlab.com/${repoPath}.git`,
|
|
361
|
+
...(fragmentRef ? { ref: fragmentRef } : {}),
|
|
362
|
+
};
|
|
363
|
+
}
|
|
364
|
+
}
|
|
365
|
+
|
|
366
|
+
// GitHub shorthand: owner/repo, owner/repo/path/to/skill, or owner/repo@skill-name
|
|
367
|
+
// Exclude paths that start with . or / to avoid matching local paths
|
|
368
|
+
// First check for @skill syntax: owner/repo@skill-name
|
|
369
|
+
const atSkillMatch = input.match(/^([^/]+)\/([^/@]+)@(.+)$/);
|
|
370
|
+
if (atSkillMatch && !input.includes(':') && !input.startsWith('.') && !input.startsWith('/')) {
|
|
371
|
+
const [, owner, repo, skillFilter] = atSkillMatch;
|
|
372
|
+
return {
|
|
373
|
+
type: 'github',
|
|
374
|
+
url: `https://github.com/${owner}/${repo}.git`,
|
|
375
|
+
...(fragmentRef ? { ref: fragmentRef } : {}),
|
|
376
|
+
skillFilter: fragmentSkillFilter || skillFilter,
|
|
377
|
+
};
|
|
378
|
+
}
|
|
379
|
+
|
|
380
|
+
const shorthandMatch = input.match(/^([^/]+)\/([^/]+)(?:\/(.+?))?\/?$/);
|
|
381
|
+
if (shorthandMatch && !input.includes(':') && !input.startsWith('.') && !input.startsWith('/')) {
|
|
382
|
+
const [, owner, repo, subpath] = shorthandMatch;
|
|
383
|
+
return {
|
|
384
|
+
type: 'github',
|
|
385
|
+
url: `https://github.com/${owner}/${repo}.git`,
|
|
386
|
+
...(fragmentRef ? { ref: fragmentRef } : {}),
|
|
387
|
+
subpath: subpath ? sanitizeSubpath(subpath) : subpath,
|
|
388
|
+
...(fragmentSkillFilter ? { skillFilter: fragmentSkillFilter } : {}),
|
|
389
|
+
};
|
|
390
|
+
}
|
|
391
|
+
|
|
392
|
+
// Well-known skills: arbitrary HTTP(S) URLs that aren't GitHub/GitLab
|
|
393
|
+
// This is the final fallback for URLs - we'll check for /.well-known/agent-skills/index.json
|
|
394
|
+
// then fall back to /.well-known/skills/index.json
|
|
395
|
+
if (isWellKnownUrl(input)) {
|
|
396
|
+
return {
|
|
397
|
+
type: 'well-known',
|
|
398
|
+
url: input,
|
|
399
|
+
};
|
|
400
|
+
}
|
|
401
|
+
|
|
402
|
+
// Fallback: treat as direct git URL
|
|
403
|
+
return {
|
|
404
|
+
type: 'git',
|
|
405
|
+
url: input,
|
|
406
|
+
...(fragmentRef ? { ref: fragmentRef } : {}),
|
|
407
|
+
};
|
|
408
|
+
}
|
|
409
|
+
|
|
410
|
+
/**
|
|
411
|
+
* Check if a URL could be a well-known skills endpoint.
|
|
412
|
+
* Must be HTTP(S) and not a known git host (GitHub, GitLab).
|
|
413
|
+
* Also excludes URLs that look like git repos (.git suffix).
|
|
414
|
+
*/
|
|
415
|
+
function isWellKnownUrl(input: string): boolean {
|
|
416
|
+
if (!input.startsWith('http://') && !input.startsWith('https://')) {
|
|
417
|
+
return false;
|
|
418
|
+
}
|
|
419
|
+
|
|
420
|
+
try {
|
|
421
|
+
const parsed = new URL(input);
|
|
422
|
+
|
|
423
|
+
// Exclude known git hosts that have their own handling
|
|
424
|
+
const excludedHosts = ['github.com', 'gitlab.com', 'raw.githubusercontent.com'];
|
|
425
|
+
if (excludedHosts.includes(parsed.hostname)) {
|
|
426
|
+
return false;
|
|
427
|
+
}
|
|
428
|
+
|
|
429
|
+
// Don't match URLs that look like git repos (should be handled by git type)
|
|
430
|
+
if (input.endsWith('.git')) {
|
|
431
|
+
return false;
|
|
432
|
+
}
|
|
433
|
+
|
|
434
|
+
return true;
|
|
435
|
+
} catch {
|
|
436
|
+
return false;
|
|
437
|
+
}
|
|
438
|
+
}
|