@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,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
+ }