@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,478 @@
1
+ import * as p from '@clack/prompts';
2
+ import pc from 'picocolors';
3
+ import { readdir, stat } from 'fs/promises';
4
+ import { join, sep } from 'path';
5
+ import { homedir } from 'os';
6
+ import { parseSkillMd } from './skills.ts';
7
+ import { installSkillForAgent, getCanonicalPath } from './installer.ts';
8
+ import {
9
+ detectInstalledAgents,
10
+ agents,
11
+ getUniversalAgents,
12
+ getVisibleUniversalAgents,
13
+ getNonUniversalAgents,
14
+ } from './agents.ts';
15
+ import { searchMultiselect } from './prompts/search-multiselect.ts';
16
+ import { addSkillToLocalLock, computeSkillFolderHash, readLocalLock } from './local-lock.ts';
17
+ import type { Skill, AgentType } from './types.ts';
18
+ import { track } from './telemetry.ts';
19
+ import { detectAgent, getAgentType } from './detect-agent.ts';
20
+
21
+ const isCancelled = (value: unknown): value is symbol => typeof value === 'symbol';
22
+
23
+ export interface SyncOptions {
24
+ agent?: string[];
25
+ yes?: boolean;
26
+ force?: boolean;
27
+ }
28
+
29
+ /**
30
+ * Shortens a path for display: replaces homedir with ~ and cwd with .
31
+ */
32
+ function shortenPath(fullPath: string, cwd: string): string {
33
+ const home = homedir();
34
+ if (fullPath === home || fullPath.startsWith(home + sep)) {
35
+ return '~' + fullPath.slice(home.length);
36
+ }
37
+ if (fullPath === cwd || fullPath.startsWith(cwd + sep)) {
38
+ return '.' + fullPath.slice(cwd.length);
39
+ }
40
+ return fullPath;
41
+ }
42
+
43
+ /**
44
+ * Crawl node_modules for SKILL.md files.
45
+ * Searches both top-level packages and scoped packages (@org/pkg).
46
+ * Returns discovered skills with their source package name.
47
+ */
48
+ async function discoverNodeModuleSkills(
49
+ cwd: string
50
+ ): Promise<Array<Skill & { packageName: string }>> {
51
+ const nodeModulesDir = join(cwd, 'node_modules');
52
+ const skills: Array<Skill & { packageName: string }> = [];
53
+
54
+ let topNames: string[];
55
+ try {
56
+ topNames = await readdir(nodeModulesDir);
57
+ } catch {
58
+ return skills;
59
+ }
60
+
61
+ const processPackageDir = async (pkgDir: string, packageName: string) => {
62
+ // Check for SKILL.md at package root
63
+ const rootSkill = await parseSkillMd(join(pkgDir, 'SKILL.md'));
64
+ if (rootSkill) {
65
+ skills.push({ ...rootSkill, packageName });
66
+ return;
67
+ }
68
+
69
+ // Check common skill locations within the package
70
+ const searchDirs = [pkgDir, join(pkgDir, 'skills'), join(pkgDir, '.agents', 'skills')];
71
+
72
+ for (const searchDir of searchDirs) {
73
+ try {
74
+ const entries = await readdir(searchDir);
75
+ for (const name of entries) {
76
+ const skillDir = join(searchDir, name);
77
+ try {
78
+ const s = await stat(skillDir);
79
+ if (!s.isDirectory()) continue;
80
+ } catch {
81
+ continue;
82
+ }
83
+ const skill = await parseSkillMd(join(skillDir, 'SKILL.md'));
84
+ if (skill) {
85
+ skills.push({ ...skill, packageName });
86
+ }
87
+ }
88
+ } catch {
89
+ // Directory doesn't exist
90
+ }
91
+ }
92
+ };
93
+
94
+ await Promise.all(
95
+ topNames.map(async (name) => {
96
+ if (name.startsWith('.')) return;
97
+
98
+ const fullPath = join(nodeModulesDir, name);
99
+ try {
100
+ const s = await stat(fullPath);
101
+ if (!s.isDirectory()) return;
102
+ } catch {
103
+ return;
104
+ }
105
+
106
+ if (name.startsWith('@')) {
107
+ // Scoped package: read @org/* entries
108
+ try {
109
+ const scopeNames = await readdir(fullPath);
110
+ await Promise.all(
111
+ scopeNames.map(async (scopedName) => {
112
+ const scopedPath = join(fullPath, scopedName);
113
+ try {
114
+ const s = await stat(scopedPath);
115
+ if (!s.isDirectory()) return;
116
+ } catch {
117
+ return;
118
+ }
119
+ await processPackageDir(scopedPath, `${name}/${scopedName}`);
120
+ })
121
+ );
122
+ } catch {
123
+ // Scope directory not readable
124
+ }
125
+ } else {
126
+ await processPackageDir(fullPath, name);
127
+ }
128
+ })
129
+ );
130
+
131
+ return skills;
132
+ }
133
+
134
+ export async function runSync(args: string[], options: SyncOptions = {}): Promise<void> {
135
+ const cwd = process.cwd();
136
+
137
+ // Auto-enable non-interactive mode when running inside an AI agent
138
+ const agentResult = await detectAgent();
139
+ if (agentResult.isAgent) {
140
+ options.yes = true;
141
+ if (!options.agent || options.agent.length === 0) {
142
+ const mappedAgent = getAgentType(agentResult.agent.name);
143
+ if (mappedAgent) {
144
+ const agentList: AgentType[] = [mappedAgent];
145
+ for (const ua of getUniversalAgents()) {
146
+ if (!agentList.includes(ua)) agentList.push(ua);
147
+ }
148
+ options.agent = agentList;
149
+ }
150
+ }
151
+ }
152
+
153
+ console.log();
154
+ if (!agentResult.isAgent) {
155
+ p.intro(pc.bgCyan(pc.black(' skills experimental_sync ')));
156
+ }
157
+
158
+ if (agentResult.isAgent) {
159
+ p.log.info(
160
+ pc.bgCyan(pc.black(pc.bold(` ${agentResult.agent.name} `))) +
161
+ ' ' +
162
+ 'Agent detected — installing non-interactively'
163
+ );
164
+ }
165
+
166
+ const spinner = p.spinner();
167
+
168
+ // 1. Discover skills from node_modules
169
+ spinner.start('Scanning node_modules for skills...');
170
+ const discoveredSkills = await discoverNodeModuleSkills(cwd);
171
+
172
+ if (discoveredSkills.length === 0) {
173
+ spinner.stop(pc.yellow('No skills found'));
174
+ p.outro(pc.dim('No SKILL.md files found in node_modules.'));
175
+ return;
176
+ }
177
+
178
+ spinner.stop(
179
+ `Found ${pc.green(String(discoveredSkills.length))} skill${discoveredSkills.length > 1 ? 's' : ''} in node_modules`
180
+ );
181
+
182
+ // Show discovered skills
183
+ for (const skill of discoveredSkills) {
184
+ p.log.info(`${pc.cyan(skill.name)} ${pc.dim(`from ${skill.packageName}`)}`);
185
+ if (skill.description) {
186
+ p.log.message(pc.dim(` ${skill.description}`));
187
+ }
188
+ }
189
+
190
+ // 2. Check which skills are already up-to-date via local lock
191
+ const localLock = await readLocalLock(cwd);
192
+ const toInstall: Array<Skill & { packageName: string }> = [];
193
+ const upToDate: string[] = [];
194
+
195
+ if (options.force) {
196
+ toInstall.push(...discoveredSkills);
197
+ p.log.info(pc.dim('Force mode: reinstalling all skills'));
198
+ } else {
199
+ for (const skill of discoveredSkills) {
200
+ const existingEntry = localLock.skills[skill.name];
201
+ if (existingEntry) {
202
+ // Compute current hash and compare
203
+ const currentHash = await computeSkillFolderHash(skill.path);
204
+ if (currentHash === existingEntry.computedHash) {
205
+ upToDate.push(skill.name);
206
+ continue;
207
+ }
208
+ }
209
+ toInstall.push(skill);
210
+ }
211
+
212
+ if (upToDate.length > 0) {
213
+ p.log.info(
214
+ pc.dim(`${upToDate.length} skill${upToDate.length !== 1 ? 's' : ''} already up to date`)
215
+ );
216
+ }
217
+
218
+ if (toInstall.length === 0) {
219
+ console.log();
220
+ p.outro(pc.green('All skills are up to date.'));
221
+ return;
222
+ }
223
+ }
224
+
225
+ p.log.info(`${toInstall.length} skill${toInstall.length !== 1 ? 's' : ''} to install/update`);
226
+
227
+ // 3. Select agents
228
+ let targetAgents: AgentType[];
229
+ const validAgents = Object.keys(agents);
230
+ const universalAgents = getUniversalAgents();
231
+ const visibleUniversalAgents = getVisibleUniversalAgents();
232
+
233
+ if (options.agent?.includes('*')) {
234
+ targetAgents = validAgents as AgentType[];
235
+ p.log.info(`Installing to all ${targetAgents.length} agents`);
236
+ } else if (options.agent && options.agent.length > 0) {
237
+ const invalidAgents = options.agent.filter((a) => !validAgents.includes(a));
238
+ if (invalidAgents.length > 0) {
239
+ p.log.error(`Invalid agents: ${invalidAgents.join(', ')}`);
240
+ p.log.info(`Valid agents: ${validAgents.join(', ')}`);
241
+ process.exit(1);
242
+ }
243
+ targetAgents = options.agent as AgentType[];
244
+ } else {
245
+ spinner.start('Loading agents...');
246
+ const installedAgents = await detectInstalledAgents();
247
+ const totalAgents = Object.keys(agents).length;
248
+ spinner.stop(`${totalAgents} agents`);
249
+
250
+ if (installedAgents.length === 0) {
251
+ if (options.yes) {
252
+ targetAgents = universalAgents;
253
+ p.log.info('Installing to universal agents');
254
+ } else {
255
+ const otherAgents = getNonUniversalAgents();
256
+
257
+ const otherChoices = otherAgents.map((a) => ({
258
+ value: a,
259
+ label: agents[a].displayName,
260
+ hint: agents[a].skillsDir,
261
+ }));
262
+
263
+ const selected = await searchMultiselect({
264
+ message: 'Which agents do you want to install to?',
265
+ items: otherChoices,
266
+ initialSelected: [],
267
+ lockedSection: {
268
+ title: 'Universal (.agents/skills)',
269
+ items: visibleUniversalAgents.map((a) => ({
270
+ value: a,
271
+ label: agents[a].displayName,
272
+ })),
273
+ hiddenCount: universalAgents.length - visibleUniversalAgents.length,
274
+ },
275
+ });
276
+
277
+ if (isCancelled(selected)) {
278
+ p.cancel('Sync cancelled');
279
+ process.exit(0);
280
+ }
281
+
282
+ targetAgents = selected as AgentType[];
283
+ }
284
+ } else if (installedAgents.length === 1 || options.yes) {
285
+ // Ensure universal agents are included
286
+ targetAgents = [...installedAgents];
287
+ for (const ua of universalAgents) {
288
+ if (!targetAgents.includes(ua)) {
289
+ targetAgents.push(ua);
290
+ }
291
+ }
292
+ } else {
293
+ const otherAgents = getNonUniversalAgents().filter((a) => installedAgents.includes(a));
294
+
295
+ const otherChoices = otherAgents.map((a) => ({
296
+ value: a,
297
+ label: agents[a].displayName,
298
+ hint: agents[a].skillsDir,
299
+ }));
300
+
301
+ const selected = await searchMultiselect({
302
+ message: 'Which agents do you want to install to?',
303
+ items: otherChoices,
304
+ initialSelected: installedAgents.filter((a) => !universalAgents.includes(a)),
305
+ lockedSection: {
306
+ title: 'Universal (.agents/skills)',
307
+ items: visibleUniversalAgents.map((a) => ({
308
+ value: a,
309
+ label: agents[a].displayName,
310
+ })),
311
+ hiddenCount: universalAgents.length - visibleUniversalAgents.length,
312
+ },
313
+ });
314
+
315
+ if (isCancelled(selected)) {
316
+ p.cancel('Sync cancelled');
317
+ process.exit(0);
318
+ }
319
+
320
+ targetAgents = selected as AgentType[];
321
+ }
322
+ }
323
+
324
+ // 4. Build summary
325
+ const summaryLines: string[] = [];
326
+ for (const skill of toInstall) {
327
+ const canonicalPath = getCanonicalPath(skill.name, { global: false });
328
+ const shortCanonical = shortenPath(canonicalPath, cwd);
329
+ summaryLines.push(`${pc.cyan(skill.name)} ${pc.dim(`← ${skill.packageName}`)}`);
330
+ summaryLines.push(` ${pc.dim(shortCanonical)}`);
331
+ }
332
+
333
+ console.log();
334
+ p.note(summaryLines.join('\n'), 'Sync Summary');
335
+
336
+ if (!options.yes) {
337
+ const confirmed = await p.confirm({ message: 'Proceed with sync?' });
338
+
339
+ if (p.isCancel(confirmed) || !confirmed) {
340
+ p.cancel('Sync cancelled');
341
+ process.exit(0);
342
+ }
343
+ }
344
+
345
+ // 5. Install skills (always project-scoped, always symlink)
346
+ spinner.start('Syncing skills...');
347
+
348
+ const results: Array<{
349
+ skill: string;
350
+ packageName: string;
351
+ agent: string;
352
+ success: boolean;
353
+ path: string;
354
+ canonicalPath?: string;
355
+ error?: string;
356
+ }> = [];
357
+
358
+ for (const skill of toInstall) {
359
+ for (const agent of targetAgents) {
360
+ const result = await installSkillForAgent(skill, agent, {
361
+ global: false,
362
+ cwd,
363
+ mode: 'symlink',
364
+ });
365
+ results.push({
366
+ skill: skill.name,
367
+ packageName: skill.packageName,
368
+ agent: agents[agent].displayName,
369
+ success: result.success,
370
+ path: result.path,
371
+ canonicalPath: result.canonicalPath,
372
+ error: result.error,
373
+ });
374
+ }
375
+ }
376
+
377
+ spinner.stop('Sync complete');
378
+
379
+ // 6. Update local lock file
380
+ const successful = results.filter((r) => r.success);
381
+ const failed = results.filter((r) => !r.success);
382
+ const successfulSkillNames = new Set(successful.map((r) => r.skill));
383
+
384
+ for (const skill of toInstall) {
385
+ if (successfulSkillNames.has(skill.name)) {
386
+ try {
387
+ const computedHash = await computeSkillFolderHash(skill.path);
388
+ await addSkillToLocalLock(
389
+ skill.name,
390
+ {
391
+ source: skill.packageName,
392
+ sourceType: 'node_modules',
393
+ computedHash,
394
+ },
395
+ cwd
396
+ );
397
+ } catch {
398
+ // Don't fail sync if lock file update fails
399
+ }
400
+ }
401
+ }
402
+
403
+ // 7. Display results
404
+ console.log();
405
+
406
+ if (successful.length > 0) {
407
+ const bySkill = new Map<string, typeof results>();
408
+ for (const r of successful) {
409
+ const skillResults = bySkill.get(r.skill) || [];
410
+ skillResults.push(r);
411
+ bySkill.set(r.skill, skillResults);
412
+ }
413
+
414
+ const resultLines: string[] = [];
415
+ for (const [skillName, skillResults] of bySkill) {
416
+ const firstResult = skillResults[0]!;
417
+ const pkg = toInstall.find((s) => s.name === skillName)?.packageName;
418
+ if (firstResult.canonicalPath) {
419
+ const shortPath = shortenPath(firstResult.canonicalPath, cwd);
420
+ resultLines.push(`${pc.green('✓')} ${skillName} ${pc.dim(`← ${pkg}`)}`);
421
+ resultLines.push(` ${pc.dim(shortPath)}`);
422
+ } else {
423
+ resultLines.push(`${pc.green('✓')} ${skillName} ${pc.dim(`← ${pkg}`)}`);
424
+ }
425
+ }
426
+
427
+ const skillCount = bySkill.size;
428
+ const title = pc.green(`Synced ${skillCount} skill${skillCount !== 1 ? 's' : ''}`);
429
+ p.note(resultLines.join('\n'), title);
430
+ }
431
+
432
+ if (failed.length > 0) {
433
+ console.log();
434
+ p.log.error(pc.red(`Failed to install ${failed.length}`));
435
+ for (const r of failed) {
436
+ p.log.message(` ${pc.red('✗')} ${r.skill} → ${r.agent}: ${pc.dim(r.error)}`);
437
+ }
438
+ }
439
+
440
+ // Track telemetry
441
+ track({
442
+ event: 'experimental_sync',
443
+ skillCount: String(toInstall.length),
444
+ successCount: String(successfulSkillNames.size),
445
+ agents: targetAgents.join(','),
446
+ });
447
+
448
+ console.log();
449
+ p.outro(
450
+ pc.green('Done!') + pc.dim(' Review skills before use; they run with full agent permissions.')
451
+ );
452
+ }
453
+
454
+ export function parseSyncOptions(args: string[]): { options: SyncOptions } {
455
+ const options: SyncOptions = {};
456
+
457
+ for (let i = 0; i < args.length; i++) {
458
+ const arg = args[i];
459
+
460
+ if (arg === '-y' || arg === '--yes') {
461
+ options.yes = true;
462
+ } else if (arg === '-f' || arg === '--force') {
463
+ options.force = true;
464
+ } else if (arg === '-a' || arg === '--agent') {
465
+ options.agent = options.agent || [];
466
+ i++;
467
+ let nextArg = args[i];
468
+ while (i < args.length && nextArg && !nextArg.startsWith('-')) {
469
+ options.agent.push(nextArg);
470
+ i++;
471
+ nextArg = args[i];
472
+ }
473
+ i--;
474
+ }
475
+ }
476
+
477
+ return { options };
478
+ }
@@ -0,0 +1,186 @@
1
+ const TELEMETRY_URL = 'https://add-skill.vercel.sh/t';
2
+ const AUDIT_URL = 'https://add-skill.vercel.sh/audit';
3
+
4
+ interface InstallTelemetryData {
5
+ event: 'install';
6
+ source: string;
7
+ skills: string;
8
+ agents: string;
9
+ global?: '1';
10
+ skillFiles?: string; // JSON stringified { skillName: relativePath }
11
+ /**
12
+ * Source type for different hosts:
13
+ * - 'github': GitHub repository (default, uses raw.githubusercontent.com)
14
+ * - 'raw': Direct URL to SKILL.md (generic raw URL)
15
+ * - Provider IDs like 'mintlify', 'huggingface', etc.
16
+ */
17
+ sourceType?: string;
18
+ }
19
+
20
+ interface RemoveTelemetryData {
21
+ event: 'remove';
22
+ source?: string;
23
+ skills: string;
24
+ agents: string;
25
+ global?: '1';
26
+ sourceType?: string;
27
+ }
28
+
29
+ interface UpdateTelemetryData {
30
+ event: 'update';
31
+ scope?: string;
32
+ skillCount: string;
33
+ successCount: string;
34
+ failCount: string;
35
+ }
36
+
37
+ interface FindTelemetryData {
38
+ event: 'find';
39
+ query: string;
40
+ resultCount: string;
41
+ interactive?: '1';
42
+ }
43
+
44
+ interface SyncTelemetryData {
45
+ event: 'experimental_sync';
46
+ skillCount: string;
47
+ successCount: string;
48
+ agents: string;
49
+ }
50
+
51
+ type TelemetryData =
52
+ | InstallTelemetryData
53
+ | RemoveTelemetryData
54
+ | UpdateTelemetryData
55
+ | FindTelemetryData
56
+ | SyncTelemetryData;
57
+
58
+ let cliVersion: string | null = null;
59
+ let detectedAgentName: string | null = null;
60
+
61
+ /**
62
+ * Set the detected AI agent name for telemetry tracking.
63
+ * Called once during agent detection, then included in all telemetry events.
64
+ */
65
+ export function setDetectedAgent(agentName: string | null): void {
66
+ detectedAgentName = agentName;
67
+ }
68
+
69
+ function isCI(): boolean {
70
+ return !!(
71
+ process.env.CI ||
72
+ process.env.GITHUB_ACTIONS ||
73
+ process.env.GITLAB_CI ||
74
+ process.env.CIRCLECI ||
75
+ process.env.TRAVIS ||
76
+ process.env.BUILDKITE ||
77
+ process.env.JENKINS_URL ||
78
+ process.env.TEAMCITY_VERSION
79
+ );
80
+ }
81
+
82
+ function isEnabled(): boolean {
83
+ return !process.env.DISABLE_TELEMETRY && !process.env.DO_NOT_TRACK;
84
+ }
85
+
86
+ export function setVersion(version: string): void {
87
+ cliVersion = version;
88
+ }
89
+
90
+ // ─── Security audit data ───
91
+
92
+ export interface PartnerAudit {
93
+ risk: 'safe' | 'low' | 'medium' | 'high' | 'critical' | 'unknown';
94
+ alerts?: number;
95
+ score?: number;
96
+ analyzedAt: string;
97
+ }
98
+
99
+ export type SkillAuditData = Record<string, PartnerAudit>;
100
+ export type AuditResponse = Record<string, SkillAuditData>;
101
+
102
+ /**
103
+ * Fetch security audit results for skills from the audit API.
104
+ * Returns null on any error or timeout — never blocks installation.
105
+ */
106
+ export async function fetchAuditData(
107
+ source: string,
108
+ skillSlugs: string[],
109
+ timeoutMs = 3000
110
+ ): Promise<AuditResponse | null> {
111
+ if (skillSlugs.length === 0) return null;
112
+
113
+ try {
114
+ const params = new URLSearchParams({
115
+ source,
116
+ skills: skillSlugs.join(','),
117
+ });
118
+
119
+ const controller = new AbortController();
120
+ const timeout = setTimeout(() => controller.abort(), timeoutMs);
121
+
122
+ const response = await fetch(`${AUDIT_URL}?${params.toString()}`, {
123
+ signal: controller.signal,
124
+ });
125
+ clearTimeout(timeout);
126
+
127
+ if (!response.ok) return null;
128
+ return (await response.json()) as AuditResponse;
129
+ } catch {
130
+ return null;
131
+ }
132
+ }
133
+
134
+ // Pending telemetry promises — awaited before CLI exit so we don't lose data,
135
+ // but never block the main workflow.
136
+ const pendingTelemetry: Promise<void>[] = [];
137
+
138
+ export function track(data: TelemetryData): void {
139
+ if (!isEnabled()) return;
140
+
141
+ try {
142
+ const params = new URLSearchParams();
143
+
144
+ // Add version
145
+ if (cliVersion) {
146
+ params.set('v', cliVersion);
147
+ }
148
+
149
+ // Add CI flag if running in CI
150
+ if (isCI()) {
151
+ params.set('ci', '1');
152
+ }
153
+
154
+ // Add detected AI agent name
155
+ if (detectedAgentName) {
156
+ params.set('agent', detectedAgentName);
157
+ }
158
+
159
+ // Add event data
160
+ for (const [key, value] of Object.entries(data)) {
161
+ if (value !== undefined && value !== null) {
162
+ params.set(key, String(value));
163
+ }
164
+ }
165
+
166
+ // Fire and forget during the workflow, but track the promise so
167
+ // flushTelemetry() can await it before the process exits.
168
+ const p = fetch(`${TELEMETRY_URL}?${params.toString()}`)
169
+ .catch(() => {})
170
+ .then(() => {});
171
+ pendingTelemetry.push(p);
172
+ } catch {
173
+ // Silently fail - telemetry should never break the CLI
174
+ }
175
+ }
176
+
177
+ /**
178
+ * Wait for all in-flight telemetry requests to settle.
179
+ * Called once at CLI exit so the process doesn't hang on open sockets
180
+ * but also doesn't drop data by exiting too early.
181
+ */
182
+ export async function flushTelemetry(timeoutMs = 5000): Promise<void> {
183
+ if (pendingTelemetry.length === 0) return;
184
+ const timeout = new Promise<void>((resolve) => setTimeout(resolve, timeoutMs));
185
+ await Promise.race([Promise.all(pendingTelemetry), timeout]);
186
+ }