@cleocode/caamp 0.3.0 → 0.4.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/README.md +18 -2
- package/dist/{chunk-PCWTRJV2.js → chunk-6HQDRJLS.js} +1135 -172
- package/dist/chunk-6HQDRJLS.js.map +1 -0
- package/dist/cli.js +966 -50
- package/dist/cli.js.map +1 -1
- package/dist/index.d.ts +330 -40
- package/dist/index.js +45 -1
- package/package.json +7 -3
- package/dist/chunk-PCWTRJV2.js.map +0 -1
|
@@ -1,58 +1,158 @@
|
|
|
1
|
-
// src/core/
|
|
2
|
-
import {
|
|
1
|
+
// src/core/paths/standard.ts
|
|
2
|
+
import { existsSync } from "fs";
|
|
3
3
|
import { homedir } from "os";
|
|
4
|
-
import { join,
|
|
5
|
-
|
|
6
|
-
function findRegistryPath() {
|
|
7
|
-
const thisDir = dirname(fileURLToPath(import.meta.url));
|
|
8
|
-
const devPath = join(thisDir, "..", "..", "..", "providers", "registry.json");
|
|
9
|
-
if (existsSync(devPath)) return devPath;
|
|
10
|
-
const distPath = join(thisDir, "..", "providers", "registry.json");
|
|
11
|
-
if (existsSync(distPath)) return distPath;
|
|
12
|
-
let dir = thisDir;
|
|
13
|
-
for (let i = 0; i < 5; i++) {
|
|
14
|
-
const candidate = join(dir, "providers", "registry.json");
|
|
15
|
-
if (existsSync(candidate)) return candidate;
|
|
16
|
-
dir = dirname(dir);
|
|
17
|
-
}
|
|
18
|
-
throw new Error(`Cannot find providers/registry.json (searched from ${thisDir})`);
|
|
19
|
-
}
|
|
20
|
-
var _registry = null;
|
|
21
|
-
var _providers = null;
|
|
22
|
-
var _aliasMap = null;
|
|
23
|
-
function getPlatformPaths() {
|
|
4
|
+
import { dirname, isAbsolute, join, resolve } from "path";
|
|
5
|
+
function getPlatformLocations() {
|
|
24
6
|
const home = homedir();
|
|
25
7
|
const platform = process.platform;
|
|
26
8
|
if (platform === "win32") {
|
|
27
9
|
const appData = process.env["APPDATA"] ?? join(home, "AppData", "Roaming");
|
|
28
10
|
return {
|
|
11
|
+
home,
|
|
29
12
|
config: appData,
|
|
30
13
|
vscodeConfig: join(appData, "Code", "User"),
|
|
31
14
|
zedConfig: join(appData, "Zed"),
|
|
32
|
-
claudeDesktopConfig: join(appData, "Claude")
|
|
15
|
+
claudeDesktopConfig: join(appData, "Claude"),
|
|
16
|
+
applications: []
|
|
33
17
|
};
|
|
34
|
-
}
|
|
18
|
+
}
|
|
19
|
+
if (platform === "darwin") {
|
|
20
|
+
const config2 = process.env["XDG_CONFIG_HOME"] ?? join(home, ".config");
|
|
35
21
|
return {
|
|
36
|
-
|
|
22
|
+
home,
|
|
23
|
+
config: config2,
|
|
37
24
|
vscodeConfig: join(home, "Library", "Application Support", "Code", "User"),
|
|
38
25
|
zedConfig: join(home, "Library", "Application Support", "Zed"),
|
|
39
|
-
claudeDesktopConfig: join(home, "Library", "Application Support", "Claude")
|
|
40
|
-
|
|
41
|
-
} else {
|
|
42
|
-
const config = process.env["XDG_CONFIG_HOME"] ?? join(home, ".config");
|
|
43
|
-
return {
|
|
44
|
-
config,
|
|
45
|
-
vscodeConfig: join(config, "Code", "User"),
|
|
46
|
-
zedConfig: join(config, "zed"),
|
|
47
|
-
claudeDesktopConfig: join(config, "Claude")
|
|
26
|
+
claudeDesktopConfig: join(home, "Library", "Application Support", "Claude"),
|
|
27
|
+
applications: ["/Applications", join(home, "Applications")]
|
|
48
28
|
};
|
|
49
29
|
}
|
|
30
|
+
const config = process.env["XDG_CONFIG_HOME"] ?? join(home, ".config");
|
|
31
|
+
return {
|
|
32
|
+
home,
|
|
33
|
+
config,
|
|
34
|
+
vscodeConfig: join(config, "Code", "User"),
|
|
35
|
+
zedConfig: join(config, "zed"),
|
|
36
|
+
claudeDesktopConfig: join(config, "Claude"),
|
|
37
|
+
applications: []
|
|
38
|
+
};
|
|
50
39
|
}
|
|
51
|
-
function
|
|
40
|
+
function normalizeHomeOverride(value) {
|
|
52
41
|
const home = homedir();
|
|
53
|
-
const
|
|
54
|
-
|
|
42
|
+
const trimmed = value.trim();
|
|
43
|
+
if (trimmed.startsWith("~/")) {
|
|
44
|
+
return join(home, trimmed.slice(2));
|
|
45
|
+
}
|
|
46
|
+
if (trimmed === "~") {
|
|
47
|
+
return home;
|
|
48
|
+
}
|
|
49
|
+
if (isAbsolute(trimmed)) {
|
|
50
|
+
return resolve(trimmed);
|
|
51
|
+
}
|
|
52
|
+
return resolve(home, trimmed);
|
|
53
|
+
}
|
|
54
|
+
function getAgentsHome() {
|
|
55
|
+
const override = process.env["AGENTS_HOME"];
|
|
56
|
+
if (override && override.trim().length > 0) {
|
|
57
|
+
return normalizeHomeOverride(override);
|
|
58
|
+
}
|
|
59
|
+
return join(homedir(), ".agents");
|
|
60
|
+
}
|
|
61
|
+
function getProjectAgentsDir(projectRoot = process.cwd()) {
|
|
62
|
+
return join(projectRoot, ".agents");
|
|
63
|
+
}
|
|
64
|
+
function resolveProjectPath(relativePath, projectDir = process.cwd()) {
|
|
65
|
+
return join(projectDir, relativePath);
|
|
66
|
+
}
|
|
67
|
+
function getCanonicalSkillsDir() {
|
|
68
|
+
return join(getAgentsHome(), "skills");
|
|
69
|
+
}
|
|
70
|
+
function getLockFilePath() {
|
|
71
|
+
return join(getAgentsHome(), ".caamp-lock.json");
|
|
72
|
+
}
|
|
73
|
+
function resolveRegistryTemplatePath(template) {
|
|
74
|
+
const locations = getPlatformLocations();
|
|
75
|
+
return template.replace(/\$HOME/g, locations.home).replace(/\$CONFIG/g, locations.config).replace(/\$VSCODE_CONFIG/g, locations.vscodeConfig).replace(/\$ZED_CONFIG/g, locations.zedConfig).replace(/\$CLAUDE_DESKTOP_CONFIG/g, locations.claudeDesktopConfig).replace(/\$AGENTS_HOME/g, getAgentsHome());
|
|
76
|
+
}
|
|
77
|
+
function resolveProviderConfigPath(provider, scope, projectDir = process.cwd()) {
|
|
78
|
+
if (scope === "global") {
|
|
79
|
+
return provider.configPathGlobal;
|
|
80
|
+
}
|
|
81
|
+
if (!provider.configPathProject) {
|
|
82
|
+
return null;
|
|
83
|
+
}
|
|
84
|
+
return resolveProjectPath(provider.configPathProject, projectDir);
|
|
85
|
+
}
|
|
86
|
+
function resolvePreferredConfigScope(provider, useGlobalFlag) {
|
|
87
|
+
if (useGlobalFlag) {
|
|
88
|
+
return "global";
|
|
89
|
+
}
|
|
90
|
+
return provider.configPathProject ? "project" : "global";
|
|
91
|
+
}
|
|
92
|
+
function resolveProviderSkillsDir(provider, scope, projectDir = process.cwd()) {
|
|
93
|
+
if (scope === "global") {
|
|
94
|
+
return provider.pathSkills;
|
|
95
|
+
}
|
|
96
|
+
return resolveProjectPath(provider.pathProjectSkills, projectDir);
|
|
97
|
+
}
|
|
98
|
+
function resolveProviderProjectPath(provider, projectDir = process.cwd()) {
|
|
99
|
+
return resolveProjectPath(provider.pathProject, projectDir);
|
|
100
|
+
}
|
|
101
|
+
function resolveProvidersRegistryPath(startDir) {
|
|
102
|
+
const candidates = [
|
|
103
|
+
join(startDir, "..", "..", "..", "providers", "registry.json"),
|
|
104
|
+
join(startDir, "..", "providers", "registry.json")
|
|
105
|
+
];
|
|
106
|
+
for (const candidate of candidates) {
|
|
107
|
+
if (existsSync(candidate)) {
|
|
108
|
+
return candidate;
|
|
109
|
+
}
|
|
110
|
+
}
|
|
111
|
+
let current = startDir;
|
|
112
|
+
for (let i = 0; i < 8; i += 1) {
|
|
113
|
+
const candidate = join(current, "providers", "registry.json");
|
|
114
|
+
if (existsSync(candidate)) {
|
|
115
|
+
return candidate;
|
|
116
|
+
}
|
|
117
|
+
current = dirname(current);
|
|
118
|
+
}
|
|
119
|
+
throw new Error(`Cannot find providers/registry.json (searched from ${startDir})`);
|
|
120
|
+
}
|
|
121
|
+
function normalizeSkillSubPath(path) {
|
|
122
|
+
if (!path) return void 0;
|
|
123
|
+
const normalized = path.replace(/\\/g, "/").replace(/^\/+/, "").replace(/\/SKILL\.md$/i, "").trim();
|
|
124
|
+
return normalized.length > 0 ? normalized : void 0;
|
|
125
|
+
}
|
|
126
|
+
function buildSkillSubPathCandidates(marketplacePath, parsedPath) {
|
|
127
|
+
const candidates = [];
|
|
128
|
+
const base = normalizeSkillSubPath(marketplacePath);
|
|
129
|
+
const parsed = normalizeSkillSubPath(parsedPath);
|
|
130
|
+
if (base) candidates.push(base);
|
|
131
|
+
if (parsed) candidates.push(parsed);
|
|
132
|
+
const knownPrefixes = [".agents", ".claude"];
|
|
133
|
+
for (const value of [base, parsed]) {
|
|
134
|
+
if (!value || !value.startsWith("skills/")) continue;
|
|
135
|
+
for (const prefix of knownPrefixes) {
|
|
136
|
+
candidates.push(`${prefix}/${value}`);
|
|
137
|
+
}
|
|
138
|
+
}
|
|
139
|
+
if (candidates.length === 0) {
|
|
140
|
+
candidates.push(void 0);
|
|
141
|
+
}
|
|
142
|
+
return Array.from(new Set(candidates));
|
|
143
|
+
}
|
|
144
|
+
|
|
145
|
+
// src/core/registry/providers.ts
|
|
146
|
+
import { readFileSync } from "fs";
|
|
147
|
+
import { dirname as dirname2 } from "path";
|
|
148
|
+
import { fileURLToPath } from "url";
|
|
149
|
+
function findRegistryPath() {
|
|
150
|
+
const thisDir = dirname2(fileURLToPath(import.meta.url));
|
|
151
|
+
return resolveProvidersRegistryPath(thisDir);
|
|
55
152
|
}
|
|
153
|
+
var _registry = null;
|
|
154
|
+
var _providers = null;
|
|
155
|
+
var _aliasMap = null;
|
|
56
156
|
function resolveProvider(raw) {
|
|
57
157
|
return {
|
|
58
158
|
id: raw.id,
|
|
@@ -60,19 +160,19 @@ function resolveProvider(raw) {
|
|
|
60
160
|
vendor: raw.vendor,
|
|
61
161
|
agentFlag: raw.agentFlag,
|
|
62
162
|
aliases: raw.aliases,
|
|
63
|
-
pathGlobal:
|
|
163
|
+
pathGlobal: resolveRegistryTemplatePath(raw.pathGlobal),
|
|
64
164
|
pathProject: raw.pathProject,
|
|
65
165
|
instructFile: raw.instructFile,
|
|
66
166
|
configKey: raw.configKey,
|
|
67
167
|
configFormat: raw.configFormat,
|
|
68
|
-
configPathGlobal:
|
|
168
|
+
configPathGlobal: resolveRegistryTemplatePath(raw.configPathGlobal),
|
|
69
169
|
configPathProject: raw.configPathProject,
|
|
70
|
-
pathSkills:
|
|
170
|
+
pathSkills: resolveRegistryTemplatePath(raw.pathSkills),
|
|
71
171
|
pathProjectSkills: raw.pathProjectSkills,
|
|
72
172
|
detection: {
|
|
73
173
|
methods: raw.detection.methods,
|
|
74
174
|
binary: raw.detection.binary,
|
|
75
|
-
directories: raw.detection.directories?.map(
|
|
175
|
+
directories: raw.detection.directories?.map(resolveRegistryTemplatePath),
|
|
76
176
|
appBundle: raw.detection.appBundle,
|
|
77
177
|
flatpakId: raw.detection.flatpakId
|
|
78
178
|
},
|
|
@@ -163,6 +263,8 @@ function isQuiet() {
|
|
|
163
263
|
import { existsSync as existsSync2 } from "fs";
|
|
164
264
|
import { execFileSync } from "child_process";
|
|
165
265
|
import { join as join2 } from "path";
|
|
266
|
+
var DEFAULT_DETECTION_CACHE_TTL_MS = 3e4;
|
|
267
|
+
var detectionCache = null;
|
|
166
268
|
function checkBinary(binary) {
|
|
167
269
|
try {
|
|
168
270
|
const cmd = process.platform === "win32" ? "where" : "which";
|
|
@@ -177,7 +279,8 @@ function checkDirectory(dir) {
|
|
|
177
279
|
}
|
|
178
280
|
function checkAppBundle(appName) {
|
|
179
281
|
if (process.platform !== "darwin") return false;
|
|
180
|
-
|
|
282
|
+
const applications = getPlatformLocations().applications;
|
|
283
|
+
return applications.some((base) => existsSync2(join2(base, appName)));
|
|
181
284
|
}
|
|
182
285
|
function checkFlatpak(flatpakId) {
|
|
183
286
|
if (process.platform !== "linux") return false;
|
|
@@ -229,24 +332,71 @@ function detectProvider(provider) {
|
|
|
229
332
|
projectDetected: false
|
|
230
333
|
};
|
|
231
334
|
}
|
|
335
|
+
function providerSignature(provider) {
|
|
336
|
+
return JSON.stringify({
|
|
337
|
+
id: provider.id,
|
|
338
|
+
methods: provider.detection.methods,
|
|
339
|
+
binary: provider.detection.binary,
|
|
340
|
+
directories: provider.detection.directories,
|
|
341
|
+
appBundle: provider.detection.appBundle,
|
|
342
|
+
flatpakId: provider.detection.flatpakId
|
|
343
|
+
});
|
|
344
|
+
}
|
|
345
|
+
function buildProvidersSignature(providers) {
|
|
346
|
+
return providers.map(providerSignature).join("|");
|
|
347
|
+
}
|
|
348
|
+
function cloneDetectionResults(results) {
|
|
349
|
+
return results.map((result) => ({
|
|
350
|
+
provider: result.provider,
|
|
351
|
+
installed: result.installed,
|
|
352
|
+
methods: [...result.methods],
|
|
353
|
+
projectDetected: result.projectDetected
|
|
354
|
+
}));
|
|
355
|
+
}
|
|
356
|
+
function getCachedResults(signature, options) {
|
|
357
|
+
if (!detectionCache || options.forceRefresh) return null;
|
|
358
|
+
if (detectionCache.signature !== signature) return null;
|
|
359
|
+
const ttlMs = options.ttlMs ?? DEFAULT_DETECTION_CACHE_TTL_MS;
|
|
360
|
+
if (ttlMs <= 0) return null;
|
|
361
|
+
if (Date.now() - detectionCache.createdAt > ttlMs) return null;
|
|
362
|
+
return cloneDetectionResults(detectionCache.results);
|
|
363
|
+
}
|
|
364
|
+
function setCachedResults(signature, results) {
|
|
365
|
+
detectionCache = {
|
|
366
|
+
createdAt: Date.now(),
|
|
367
|
+
signature,
|
|
368
|
+
results: cloneDetectionResults(results)
|
|
369
|
+
};
|
|
370
|
+
}
|
|
232
371
|
function detectProjectProvider(provider, projectDir) {
|
|
233
372
|
if (!provider.pathProject) return false;
|
|
234
|
-
return existsSync2(
|
|
373
|
+
return existsSync2(resolveProviderProjectPath(provider, projectDir));
|
|
235
374
|
}
|
|
236
|
-
function detectAllProviders() {
|
|
375
|
+
function detectAllProviders(options = {}) {
|
|
237
376
|
const providers = getAllProviders();
|
|
238
|
-
|
|
377
|
+
const signature = buildProvidersSignature(providers);
|
|
378
|
+
const cached = getCachedResults(signature, options);
|
|
379
|
+
if (cached) {
|
|
380
|
+
debug(`detection cache hit for ${providers.length} providers`);
|
|
381
|
+
return cached;
|
|
382
|
+
}
|
|
383
|
+
const results = providers.map(detectProvider);
|
|
384
|
+
setCachedResults(signature, results);
|
|
385
|
+
return cloneDetectionResults(results);
|
|
239
386
|
}
|
|
240
|
-
function getInstalledProviders() {
|
|
241
|
-
return detectAllProviders().filter((r) => r.installed).map((r) => r.provider);
|
|
387
|
+
function getInstalledProviders(options = {}) {
|
|
388
|
+
return detectAllProviders(options).filter((r) => r.installed).map((r) => r.provider);
|
|
242
389
|
}
|
|
243
|
-
function detectProjectProviders(projectDir) {
|
|
244
|
-
const results = detectAllProviders();
|
|
390
|
+
function detectProjectProviders(projectDir, options = {}) {
|
|
391
|
+
const results = detectAllProviders(options);
|
|
245
392
|
return results.map((r) => ({
|
|
246
393
|
...r,
|
|
247
394
|
projectDetected: detectProjectProvider(r.provider, projectDir)
|
|
248
395
|
}));
|
|
249
396
|
}
|
|
397
|
+
function resetDetectionCache() {
|
|
398
|
+
detectionCache = null;
|
|
399
|
+
}
|
|
250
400
|
|
|
251
401
|
// src/core/sources/parser.ts
|
|
252
402
|
var GITHUB_SHORTHAND = /^([a-zA-Z0-9_.-]+)\/([a-zA-Z0-9_.-]+)(?:\/(.+))?$/;
|
|
@@ -368,15 +518,20 @@ function isMarketplaceScoped(input) {
|
|
|
368
518
|
// src/core/skills/installer.ts
|
|
369
519
|
import { mkdir, symlink, rm, cp } from "fs/promises";
|
|
370
520
|
import { existsSync as existsSync3, lstatSync } from "fs";
|
|
371
|
-
import { homedir as homedir2 } from "os";
|
|
372
521
|
import { join as join3 } from "path";
|
|
373
|
-
|
|
522
|
+
|
|
523
|
+
// src/core/paths/agents.ts
|
|
524
|
+
var AGENTS_HOME = getAgentsHome();
|
|
525
|
+
var LOCK_FILE_PATH = getLockFilePath();
|
|
526
|
+
var CANONICAL_SKILLS_DIR = getCanonicalSkillsDir();
|
|
527
|
+
|
|
528
|
+
// src/core/skills/installer.ts
|
|
374
529
|
async function ensureCanonicalDir() {
|
|
375
|
-
await mkdir(
|
|
530
|
+
await mkdir(CANONICAL_SKILLS_DIR, { recursive: true });
|
|
376
531
|
}
|
|
377
532
|
async function installToCanonical(sourcePath, skillName) {
|
|
378
533
|
await ensureCanonicalDir();
|
|
379
|
-
const targetDir = join3(
|
|
534
|
+
const targetDir = join3(CANONICAL_SKILLS_DIR, skillName);
|
|
380
535
|
if (existsSync3(targetDir)) {
|
|
381
536
|
await rm(targetDir, { recursive: true });
|
|
382
537
|
}
|
|
@@ -384,7 +539,11 @@ async function installToCanonical(sourcePath, skillName) {
|
|
|
384
539
|
return targetDir;
|
|
385
540
|
}
|
|
386
541
|
async function linkToAgent(canonicalPath, provider, skillName, isGlobal, projectDir) {
|
|
387
|
-
const targetSkillsDir =
|
|
542
|
+
const targetSkillsDir = resolveProviderSkillsDir(
|
|
543
|
+
provider,
|
|
544
|
+
isGlobal ? "global" : "project",
|
|
545
|
+
projectDir
|
|
546
|
+
);
|
|
388
547
|
if (!targetSkillsDir) {
|
|
389
548
|
return { success: false, error: `Provider ${provider.id} has no skills directory` };
|
|
390
549
|
}
|
|
@@ -437,7 +596,11 @@ async function removeSkill(skillName, providers, isGlobal, projectDir) {
|
|
|
437
596
|
const removed = [];
|
|
438
597
|
const errors = [];
|
|
439
598
|
for (const provider of providers) {
|
|
440
|
-
const skillsDir =
|
|
599
|
+
const skillsDir = resolveProviderSkillsDir(
|
|
600
|
+
provider,
|
|
601
|
+
isGlobal ? "global" : "project",
|
|
602
|
+
projectDir
|
|
603
|
+
);
|
|
441
604
|
if (!skillsDir) continue;
|
|
442
605
|
const linkPath = join3(skillsDir, skillName);
|
|
443
606
|
if (existsSync3(linkPath)) {
|
|
@@ -449,7 +612,7 @@ async function removeSkill(skillName, providers, isGlobal, projectDir) {
|
|
|
449
612
|
}
|
|
450
613
|
}
|
|
451
614
|
}
|
|
452
|
-
const canonicalPath = join3(
|
|
615
|
+
const canonicalPath = join3(CANONICAL_SKILLS_DIR, skillName);
|
|
453
616
|
if (existsSync3(canonicalPath)) {
|
|
454
617
|
try {
|
|
455
618
|
await rm(canonicalPath, { recursive: true });
|
|
@@ -460,62 +623,95 @@ async function removeSkill(skillName, providers, isGlobal, projectDir) {
|
|
|
460
623
|
return { removed, errors };
|
|
461
624
|
}
|
|
462
625
|
async function listCanonicalSkills() {
|
|
463
|
-
if (!existsSync3(
|
|
626
|
+
if (!existsSync3(CANONICAL_SKILLS_DIR)) return [];
|
|
464
627
|
const { readdir: readdir2 } = await import("fs/promises");
|
|
465
|
-
const entries = await readdir2(
|
|
628
|
+
const entries = await readdir2(CANONICAL_SKILLS_DIR, { withFileTypes: true });
|
|
466
629
|
return entries.filter((e) => e.isDirectory() || e.isSymbolicLink()).map((e) => e.name);
|
|
467
630
|
}
|
|
468
631
|
|
|
469
632
|
// src/core/lock-utils.ts
|
|
470
|
-
import { readFile as readFile2, writeFile as writeFile2, mkdir as mkdir2 } from "fs/promises";
|
|
633
|
+
import { open, readFile as readFile2, writeFile as writeFile2, mkdir as mkdir2, rm as rm2, rename } from "fs/promises";
|
|
471
634
|
import { existsSync as existsSync4 } from "fs";
|
|
472
|
-
|
|
473
|
-
|
|
474
|
-
|
|
475
|
-
|
|
635
|
+
var LOCK_GUARD_PATH = `${LOCK_FILE_PATH}.lock`;
|
|
636
|
+
function sleep(ms) {
|
|
637
|
+
return new Promise((resolve3) => setTimeout(resolve3, ms));
|
|
638
|
+
}
|
|
639
|
+
async function acquireLockGuard(retries = 40, delayMs = 25) {
|
|
640
|
+
await mkdir2(AGENTS_HOME, { recursive: true });
|
|
641
|
+
for (let attempt = 0; attempt < retries; attempt += 1) {
|
|
642
|
+
try {
|
|
643
|
+
const handle = await open(LOCK_GUARD_PATH, "wx");
|
|
644
|
+
await handle.close();
|
|
645
|
+
return;
|
|
646
|
+
} catch (error) {
|
|
647
|
+
if (!(error instanceof Error) || !("code" in error) || error.code !== "EEXIST") {
|
|
648
|
+
throw error;
|
|
649
|
+
}
|
|
650
|
+
await sleep(delayMs);
|
|
651
|
+
}
|
|
652
|
+
}
|
|
653
|
+
throw new Error("Timed out waiting for lock file guard");
|
|
654
|
+
}
|
|
655
|
+
async function releaseLockGuard() {
|
|
656
|
+
await rm2(LOCK_GUARD_PATH, { force: true });
|
|
657
|
+
}
|
|
658
|
+
async function writeLockFileUnsafe(lock) {
|
|
659
|
+
const tmpPath = `${LOCK_FILE_PATH}.tmp-${process.pid}-${Date.now()}`;
|
|
660
|
+
await writeFile2(tmpPath, JSON.stringify(lock, null, 2) + "\n", "utf-8");
|
|
661
|
+
await rename(tmpPath, LOCK_FILE_PATH);
|
|
662
|
+
}
|
|
476
663
|
async function readLockFile() {
|
|
477
664
|
try {
|
|
478
|
-
if (!existsSync4(
|
|
665
|
+
if (!existsSync4(LOCK_FILE_PATH)) {
|
|
479
666
|
return { version: 1, skills: {}, mcpServers: {} };
|
|
480
667
|
}
|
|
481
|
-
const content = await readFile2(
|
|
668
|
+
const content = await readFile2(LOCK_FILE_PATH, "utf-8");
|
|
482
669
|
return JSON.parse(content);
|
|
483
670
|
} catch {
|
|
484
671
|
return { version: 1, skills: {}, mcpServers: {} };
|
|
485
672
|
}
|
|
486
673
|
}
|
|
487
|
-
async function
|
|
488
|
-
await
|
|
489
|
-
|
|
674
|
+
async function updateLockFile(updater) {
|
|
675
|
+
await acquireLockGuard();
|
|
676
|
+
try {
|
|
677
|
+
const lock = await readLockFile();
|
|
678
|
+
await updater(lock);
|
|
679
|
+
await writeLockFileUnsafe(lock);
|
|
680
|
+
return lock;
|
|
681
|
+
} finally {
|
|
682
|
+
await releaseLockGuard();
|
|
683
|
+
}
|
|
490
684
|
}
|
|
491
685
|
|
|
492
686
|
// src/core/skills/lock.ts
|
|
493
687
|
import { simpleGit } from "simple-git";
|
|
494
688
|
async function recordSkillInstall(skillName, scopedName, source, sourceType, agents, canonicalPath, isGlobal, projectDir, version) {
|
|
495
|
-
|
|
496
|
-
|
|
497
|
-
|
|
498
|
-
|
|
499
|
-
|
|
500
|
-
|
|
501
|
-
|
|
502
|
-
|
|
503
|
-
|
|
504
|
-
|
|
505
|
-
|
|
506
|
-
|
|
507
|
-
|
|
508
|
-
|
|
509
|
-
|
|
510
|
-
|
|
511
|
-
|
|
689
|
+
await updateLockFile((lock) => {
|
|
690
|
+
const now = (/* @__PURE__ */ new Date()).toISOString();
|
|
691
|
+
const existing = lock.skills[skillName];
|
|
692
|
+
lock.skills[skillName] = {
|
|
693
|
+
name: skillName,
|
|
694
|
+
scopedName,
|
|
695
|
+
source,
|
|
696
|
+
sourceType,
|
|
697
|
+
version,
|
|
698
|
+
installedAt: existing?.installedAt ?? now,
|
|
699
|
+
updatedAt: now,
|
|
700
|
+
agents: [.../* @__PURE__ */ new Set([...existing?.agents ?? [], ...agents])],
|
|
701
|
+
canonicalPath,
|
|
702
|
+
isGlobal,
|
|
703
|
+
projectDir
|
|
704
|
+
};
|
|
705
|
+
});
|
|
512
706
|
}
|
|
513
707
|
async function removeSkillFromLock(skillName) {
|
|
514
|
-
|
|
515
|
-
|
|
516
|
-
|
|
517
|
-
|
|
518
|
-
|
|
708
|
+
let removed = false;
|
|
709
|
+
await updateLockFile((lock) => {
|
|
710
|
+
if (!(skillName in lock.skills)) return;
|
|
711
|
+
delete lock.skills[skillName];
|
|
712
|
+
removed = true;
|
|
713
|
+
});
|
|
714
|
+
return removed;
|
|
519
715
|
}
|
|
520
716
|
async function getTrackedSkills() {
|
|
521
717
|
const lock = await readLockFile();
|
|
@@ -576,8 +772,66 @@ async function checkSkillUpdate(skillName) {
|
|
|
576
772
|
};
|
|
577
773
|
}
|
|
578
774
|
|
|
775
|
+
// src/core/network/fetch.ts
|
|
776
|
+
var DEFAULT_FETCH_TIMEOUT_MS = 1e4;
|
|
777
|
+
var NetworkError = class extends Error {
|
|
778
|
+
kind;
|
|
779
|
+
url;
|
|
780
|
+
status;
|
|
781
|
+
constructor(message, kind, url, status) {
|
|
782
|
+
super(message);
|
|
783
|
+
this.name = "NetworkError";
|
|
784
|
+
this.kind = kind;
|
|
785
|
+
this.url = url;
|
|
786
|
+
this.status = status;
|
|
787
|
+
}
|
|
788
|
+
};
|
|
789
|
+
function isAbortError(error) {
|
|
790
|
+
return error instanceof Error && error.name === "AbortError";
|
|
791
|
+
}
|
|
792
|
+
async function fetchWithTimeout(url, init, timeoutMs = DEFAULT_FETCH_TIMEOUT_MS) {
|
|
793
|
+
try {
|
|
794
|
+
return await fetch(url, {
|
|
795
|
+
...init,
|
|
796
|
+
signal: AbortSignal.timeout(timeoutMs)
|
|
797
|
+
});
|
|
798
|
+
} catch (error) {
|
|
799
|
+
if (isAbortError(error)) {
|
|
800
|
+
throw new NetworkError(`Request timed out after ${timeoutMs}ms`, "timeout", url);
|
|
801
|
+
}
|
|
802
|
+
throw new NetworkError("Network request failed", "network", url);
|
|
803
|
+
}
|
|
804
|
+
}
|
|
805
|
+
function ensureOkResponse(response, url) {
|
|
806
|
+
if (!response.ok) {
|
|
807
|
+
throw new NetworkError(`Request failed with status ${response.status}`, "http", url, response.status);
|
|
808
|
+
}
|
|
809
|
+
return response;
|
|
810
|
+
}
|
|
811
|
+
function formatNetworkError(error) {
|
|
812
|
+
if (error instanceof NetworkError) {
|
|
813
|
+
if (error.kind === "timeout") {
|
|
814
|
+
return "Network request timed out. Please check your connection and try again.";
|
|
815
|
+
}
|
|
816
|
+
if (error.kind === "http") {
|
|
817
|
+
return `Marketplace request failed with HTTP ${error.status ?? "unknown"}. Please try again shortly.`;
|
|
818
|
+
}
|
|
819
|
+
return "Network request failed. Please check your connection and try again.";
|
|
820
|
+
}
|
|
821
|
+
if (error instanceof Error) return error.message;
|
|
822
|
+
return String(error);
|
|
823
|
+
}
|
|
824
|
+
|
|
579
825
|
// src/core/marketplace/skillsmp.ts
|
|
580
826
|
var API_BASE = "https://www.agentskills.in/api/skills";
|
|
827
|
+
function parseScopedName(value) {
|
|
828
|
+
const match = value.match(/^@([^/]+)\/([^/]+)$/);
|
|
829
|
+
if (!match) return null;
|
|
830
|
+
return {
|
|
831
|
+
author: match[1],
|
|
832
|
+
name: match[2]
|
|
833
|
+
};
|
|
834
|
+
}
|
|
581
835
|
function toResult(skill) {
|
|
582
836
|
return {
|
|
583
837
|
name: skill.name,
|
|
@@ -599,31 +853,34 @@ var SkillsMPAdapter = class {
|
|
|
599
853
|
limit: String(limit),
|
|
600
854
|
sortBy: "stars"
|
|
601
855
|
});
|
|
602
|
-
|
|
603
|
-
|
|
604
|
-
|
|
605
|
-
|
|
606
|
-
return data.skills.map(toResult);
|
|
607
|
-
} catch {
|
|
608
|
-
return [];
|
|
609
|
-
}
|
|
856
|
+
const url = `${API_BASE}?${params}`;
|
|
857
|
+
const response = ensureOkResponse(await fetchWithTimeout(url), url);
|
|
858
|
+
const data = await response.json();
|
|
859
|
+
return data.skills.map(toResult);
|
|
610
860
|
}
|
|
611
861
|
async getSkill(scopedName) {
|
|
612
|
-
const
|
|
613
|
-
|
|
614
|
-
|
|
615
|
-
|
|
616
|
-
|
|
617
|
-
|
|
618
|
-
|
|
862
|
+
const parts = parseScopedName(scopedName);
|
|
863
|
+
const searchTerms = parts ? [parts.name, `${parts.author} ${parts.name}`, scopedName] : [scopedName];
|
|
864
|
+
const seen = /* @__PURE__ */ new Set();
|
|
865
|
+
for (const term of searchTerms) {
|
|
866
|
+
if (seen.has(term)) continue;
|
|
867
|
+
seen.add(term);
|
|
868
|
+
const params = new URLSearchParams({
|
|
869
|
+
search: term,
|
|
870
|
+
limit: "50",
|
|
871
|
+
sortBy: "stars"
|
|
872
|
+
});
|
|
873
|
+
const url = `${API_BASE}?${params}`;
|
|
874
|
+
const response = ensureOkResponse(await fetchWithTimeout(url), url);
|
|
619
875
|
const data = await response.json();
|
|
620
876
|
const match = data.skills.find(
|
|
621
877
|
(s) => s.scopedName === scopedName || `@${s.author}/${s.name}` === scopedName
|
|
622
878
|
);
|
|
623
|
-
|
|
624
|
-
|
|
625
|
-
|
|
879
|
+
if (match) {
|
|
880
|
+
return toResult(match);
|
|
881
|
+
}
|
|
626
882
|
}
|
|
883
|
+
return null;
|
|
627
884
|
}
|
|
628
885
|
};
|
|
629
886
|
|
|
@@ -645,18 +902,14 @@ function toResult2(skill) {
|
|
|
645
902
|
var SkillsShAdapter = class {
|
|
646
903
|
name = "skills.sh";
|
|
647
904
|
async search(query, limit = 20) {
|
|
648
|
-
|
|
649
|
-
|
|
650
|
-
|
|
651
|
-
|
|
652
|
-
|
|
653
|
-
|
|
654
|
-
|
|
655
|
-
|
|
656
|
-
return data.results.map(toResult2);
|
|
657
|
-
} catch {
|
|
658
|
-
return [];
|
|
659
|
-
}
|
|
905
|
+
const params = new URLSearchParams({
|
|
906
|
+
q: query,
|
|
907
|
+
limit: String(limit)
|
|
908
|
+
});
|
|
909
|
+
const url = `${API_BASE2}/search?${params}`;
|
|
910
|
+
const response = ensureOkResponse(await fetchWithTimeout(url), url);
|
|
911
|
+
const data = await response.json();
|
|
912
|
+
return data.results.map(toResult2);
|
|
660
913
|
}
|
|
661
914
|
async getSkill(scopedName) {
|
|
662
915
|
const results = await this.search(scopedName, 5);
|
|
@@ -665,6 +918,14 @@ var SkillsShAdapter = class {
|
|
|
665
918
|
};
|
|
666
919
|
|
|
667
920
|
// src/core/marketplace/client.ts
|
|
921
|
+
var MarketplaceUnavailableError = class extends Error {
|
|
922
|
+
details;
|
|
923
|
+
constructor(message, details) {
|
|
924
|
+
super(message);
|
|
925
|
+
this.name = "MarketplaceUnavailableError";
|
|
926
|
+
this.details = details;
|
|
927
|
+
}
|
|
928
|
+
};
|
|
668
929
|
var MarketplaceClient = class {
|
|
669
930
|
adapters;
|
|
670
931
|
/**
|
|
@@ -704,11 +965,21 @@ var MarketplaceClient = class {
|
|
|
704
965
|
* ```
|
|
705
966
|
*/
|
|
706
967
|
async search(query, limit = 20) {
|
|
707
|
-
const
|
|
708
|
-
|
|
709
|
-
|
|
710
|
-
const
|
|
711
|
-
|
|
968
|
+
const settled = await Promise.allSettled(this.adapters.map((adapter) => adapter.search(query, limit)));
|
|
969
|
+
const flat = [];
|
|
970
|
+
const failures = [];
|
|
971
|
+
for (const [index, result] of settled.entries()) {
|
|
972
|
+
const adapterName = this.adapters[index]?.name ?? "unknown";
|
|
973
|
+
if (result.status === "fulfilled") {
|
|
974
|
+
flat.push(...result.value);
|
|
975
|
+
} else {
|
|
976
|
+
const reason = result.reason instanceof Error ? result.reason.message : String(result.reason);
|
|
977
|
+
failures.push(`${adapterName}: ${reason}`);
|
|
978
|
+
}
|
|
979
|
+
}
|
|
980
|
+
if (flat.length === 0 && failures.length > 0) {
|
|
981
|
+
throw new MarketplaceUnavailableError("All marketplace sources failed.", failures);
|
|
982
|
+
}
|
|
712
983
|
const seen = /* @__PURE__ */ new Map();
|
|
713
984
|
for (const result of flat) {
|
|
714
985
|
const existing = seen.get(result.scopedName);
|
|
@@ -734,9 +1005,18 @@ var MarketplaceClient = class {
|
|
|
734
1005
|
* ```
|
|
735
1006
|
*/
|
|
736
1007
|
async getSkill(scopedName) {
|
|
1008
|
+
const failures = [];
|
|
737
1009
|
for (const adapter of this.adapters) {
|
|
738
|
-
|
|
739
|
-
|
|
1010
|
+
try {
|
|
1011
|
+
const result = await adapter.getSkill(scopedName);
|
|
1012
|
+
if (result) return result;
|
|
1013
|
+
} catch (error) {
|
|
1014
|
+
const reason = error instanceof Error ? error.message : String(error);
|
|
1015
|
+
failures.push(`${adapter.name}: ${reason}`);
|
|
1016
|
+
}
|
|
1017
|
+
}
|
|
1018
|
+
if (failures.length === this.adapters.length && this.adapters.length > 0) {
|
|
1019
|
+
throw new MarketplaceUnavailableError("All marketplace sources failed.", failures);
|
|
740
1020
|
}
|
|
741
1021
|
return null;
|
|
742
1022
|
}
|
|
@@ -745,7 +1025,7 @@ var MarketplaceClient = class {
|
|
|
745
1025
|
// src/core/skills/discovery.ts
|
|
746
1026
|
import { readFile as readFile3, readdir } from "fs/promises";
|
|
747
1027
|
import { existsSync as existsSync5 } from "fs";
|
|
748
|
-
import { join as
|
|
1028
|
+
import { join as join4 } from "path";
|
|
749
1029
|
import matter from "gray-matter";
|
|
750
1030
|
async function parseSkillFile(filePath) {
|
|
751
1031
|
try {
|
|
@@ -769,7 +1049,7 @@ async function parseSkillFile(filePath) {
|
|
|
769
1049
|
}
|
|
770
1050
|
}
|
|
771
1051
|
async function discoverSkill(skillDir) {
|
|
772
|
-
const skillFile =
|
|
1052
|
+
const skillFile = join4(skillDir, "SKILL.md");
|
|
773
1053
|
if (!existsSync5(skillFile)) return null;
|
|
774
1054
|
const metadata = await parseSkillFile(skillFile);
|
|
775
1055
|
if (!metadata) return null;
|
|
@@ -786,7 +1066,7 @@ async function discoverSkills(rootDir) {
|
|
|
786
1066
|
const skills = [];
|
|
787
1067
|
for (const entry of entries) {
|
|
788
1068
|
if (!entry.isDirectory() && !entry.isSymbolicLink()) continue;
|
|
789
|
-
const skillDir =
|
|
1069
|
+
const skillDir = join4(rootDir, entry.name);
|
|
790
1070
|
const skill = await discoverSkill(skillDir);
|
|
791
1071
|
if (skill) {
|
|
792
1072
|
skills.push(skill);
|
|
@@ -809,6 +1089,273 @@ async function discoverSkillsMulti(dirs) {
|
|
|
809
1089
|
return all;
|
|
810
1090
|
}
|
|
811
1091
|
|
|
1092
|
+
// src/core/skills/recommendation.ts
|
|
1093
|
+
var RECOMMENDATION_ERROR_CODES = {
|
|
1094
|
+
QUERY_INVALID: "E_SKILLS_QUERY_INVALID",
|
|
1095
|
+
NO_MATCHES: "E_SKILLS_NO_MATCHES",
|
|
1096
|
+
SOURCE_UNAVAILABLE: "E_SKILLS_SOURCE_UNAVAILABLE",
|
|
1097
|
+
CRITERIA_CONFLICT: "E_SKILLS_CRITERIA_CONFLICT"
|
|
1098
|
+
};
|
|
1099
|
+
var DEFAULT_WEIGHTS = {
|
|
1100
|
+
mustHaveMatch: 10,
|
|
1101
|
+
preferMatch: 4,
|
|
1102
|
+
queryTokenMatch: 3,
|
|
1103
|
+
starsFactor: 2,
|
|
1104
|
+
metadataBoost: 2,
|
|
1105
|
+
modernMarkerBoost: 3,
|
|
1106
|
+
legacyMarkerPenalty: 3,
|
|
1107
|
+
excludePenalty: 25,
|
|
1108
|
+
missingMustHavePenalty: 20
|
|
1109
|
+
};
|
|
1110
|
+
var DEFAULT_MODERN_MARKERS = ["svelte 5", "runes", "lafs", "slsa", "drizzle", "better-auth"];
|
|
1111
|
+
var DEFAULT_LEGACY_MARKERS = ["svelte 3", "jquery", "bower", "legacy", "book.json", "gitbook-cli"];
|
|
1112
|
+
function tokenizeCriteriaValue(value) {
|
|
1113
|
+
return value.split(",").map((part) => part.trim().toLowerCase()).filter(Boolean);
|
|
1114
|
+
}
|
|
1115
|
+
function normalizeList(value) {
|
|
1116
|
+
if (value === void 0) return [];
|
|
1117
|
+
if (!(typeof value === "string" || Array.isArray(value))) return [];
|
|
1118
|
+
const source = Array.isArray(value) ? value : [value];
|
|
1119
|
+
const flattened = source.flatMap((item) => typeof item === "string" ? tokenizeCriteriaValue(item) : []);
|
|
1120
|
+
return Array.from(new Set(flattened)).sort((a, b) => a.localeCompare(b));
|
|
1121
|
+
}
|
|
1122
|
+
function hasAnyCriteriaInput(input) {
|
|
1123
|
+
const query = typeof input.query === "string" ? input.query.trim() : "";
|
|
1124
|
+
if (query.length > 0) return true;
|
|
1125
|
+
const lists = [input.mustHave, input.prefer, input.exclude];
|
|
1126
|
+
return lists.some((list) => normalizeList(list).length > 0);
|
|
1127
|
+
}
|
|
1128
|
+
function validateRecommendationCriteria(input) {
|
|
1129
|
+
const issues = [];
|
|
1130
|
+
if (input.query !== void 0 && typeof input.query !== "string") {
|
|
1131
|
+
issues.push({
|
|
1132
|
+
code: RECOMMENDATION_ERROR_CODES.QUERY_INVALID,
|
|
1133
|
+
field: "query",
|
|
1134
|
+
message: "query must be a string"
|
|
1135
|
+
});
|
|
1136
|
+
}
|
|
1137
|
+
if (input.mustHave !== void 0 && !(typeof input.mustHave === "string" || Array.isArray(input.mustHave))) {
|
|
1138
|
+
issues.push({
|
|
1139
|
+
code: RECOMMENDATION_ERROR_CODES.QUERY_INVALID,
|
|
1140
|
+
field: "mustHave",
|
|
1141
|
+
message: "mustHave must be a string or string[]"
|
|
1142
|
+
});
|
|
1143
|
+
}
|
|
1144
|
+
if (input.prefer !== void 0 && !(typeof input.prefer === "string" || Array.isArray(input.prefer))) {
|
|
1145
|
+
issues.push({
|
|
1146
|
+
code: RECOMMENDATION_ERROR_CODES.QUERY_INVALID,
|
|
1147
|
+
field: "prefer",
|
|
1148
|
+
message: "prefer must be a string or string[]"
|
|
1149
|
+
});
|
|
1150
|
+
}
|
|
1151
|
+
if (input.exclude !== void 0 && !(typeof input.exclude === "string" || Array.isArray(input.exclude))) {
|
|
1152
|
+
issues.push({
|
|
1153
|
+
code: RECOMMENDATION_ERROR_CODES.QUERY_INVALID,
|
|
1154
|
+
field: "exclude",
|
|
1155
|
+
message: "exclude must be a string or string[]"
|
|
1156
|
+
});
|
|
1157
|
+
}
|
|
1158
|
+
const mustHave = normalizeList(input.mustHave);
|
|
1159
|
+
const prefer = normalizeList(input.prefer);
|
|
1160
|
+
const exclude = normalizeList(input.exclude);
|
|
1161
|
+
const conflict = mustHave.some((term) => exclude.includes(term)) || prefer.some((term) => exclude.includes(term));
|
|
1162
|
+
if (conflict) {
|
|
1163
|
+
issues.push({
|
|
1164
|
+
code: RECOMMENDATION_ERROR_CODES.CRITERIA_CONFLICT,
|
|
1165
|
+
field: "exclude",
|
|
1166
|
+
message: "criteria terms cannot appear in both prefer/must-have and exclude"
|
|
1167
|
+
});
|
|
1168
|
+
}
|
|
1169
|
+
if (issues.length === 0 && !hasAnyCriteriaInput(input)) {
|
|
1170
|
+
issues.push({
|
|
1171
|
+
code: RECOMMENDATION_ERROR_CODES.QUERY_INVALID,
|
|
1172
|
+
field: "query",
|
|
1173
|
+
message: "at least one criteria value is required"
|
|
1174
|
+
});
|
|
1175
|
+
}
|
|
1176
|
+
return {
|
|
1177
|
+
valid: issues.length === 0,
|
|
1178
|
+
issues
|
|
1179
|
+
};
|
|
1180
|
+
}
|
|
1181
|
+
function normalizeRecommendationCriteria(input) {
|
|
1182
|
+
const query = (input.query ?? "").trim().toLowerCase();
|
|
1183
|
+
return {
|
|
1184
|
+
query,
|
|
1185
|
+
queryTokens: query ? Array.from(new Set(tokenizeCriteriaValue(query.replace(/\s+/g, ",")))).sort((a, b) => a.localeCompare(b)) : [],
|
|
1186
|
+
mustHave: normalizeList(input.mustHave),
|
|
1187
|
+
prefer: normalizeList(input.prefer),
|
|
1188
|
+
exclude: normalizeList(input.exclude)
|
|
1189
|
+
};
|
|
1190
|
+
}
|
|
1191
|
+
function countMatches(haystack, needles) {
|
|
1192
|
+
let count = 0;
|
|
1193
|
+
for (const needle of needles) {
|
|
1194
|
+
if (haystack.includes(needle)) {
|
|
1195
|
+
count += 1;
|
|
1196
|
+
}
|
|
1197
|
+
}
|
|
1198
|
+
return count;
|
|
1199
|
+
}
|
|
1200
|
+
function clampScore(value) {
|
|
1201
|
+
return Number(value.toFixed(6));
|
|
1202
|
+
}
|
|
1203
|
+
function buildSearchText(skill) {
|
|
1204
|
+
return `${skill.name} ${skill.scopedName} ${skill.description} ${skill.author}`.toLowerCase();
|
|
1205
|
+
}
|
|
1206
|
+
function scoreSkillRecommendation(skill, criteria, options = {}) {
|
|
1207
|
+
const weights = { ...DEFAULT_WEIGHTS, ...options.weights };
|
|
1208
|
+
const modernMarkers = (options.modernMarkers ?? DEFAULT_MODERN_MARKERS).map((marker) => marker.toLowerCase());
|
|
1209
|
+
const legacyMarkers = (options.legacyMarkers ?? DEFAULT_LEGACY_MARKERS).map((marker) => marker.toLowerCase());
|
|
1210
|
+
const text = buildSearchText(skill);
|
|
1211
|
+
const reasons = [];
|
|
1212
|
+
const tradeoffs = [];
|
|
1213
|
+
const mustHaveMatches = countMatches(text, criteria.mustHave);
|
|
1214
|
+
const missingMustHave = Math.max(criteria.mustHave.length - mustHaveMatches, 0);
|
|
1215
|
+
const preferMatches = countMatches(text, criteria.prefer);
|
|
1216
|
+
const queryMatches = countMatches(text, criteria.queryTokens);
|
|
1217
|
+
const excludeMatches = countMatches(text, criteria.exclude);
|
|
1218
|
+
const modernMatches = countMatches(text, modernMarkers);
|
|
1219
|
+
const legacyMatches = countMatches(text, legacyMarkers);
|
|
1220
|
+
const metadataSignal = skill.description.trim().length >= 80 ? 1 : 0;
|
|
1221
|
+
const starsSignal = Math.log10(skill.stars + 1);
|
|
1222
|
+
const sourceConfidence = skill.source === "agentskills.in" ? 1 : skill.source === "skills.sh" ? 0.8 : 0.6;
|
|
1223
|
+
const mustHaveScore = mustHaveMatches * weights.mustHaveMatch - missingMustHave * weights.missingMustHavePenalty;
|
|
1224
|
+
const preferScore = preferMatches * weights.preferMatch;
|
|
1225
|
+
const queryScore = queryMatches * weights.queryTokenMatch;
|
|
1226
|
+
const starsScore = starsSignal * weights.starsFactor;
|
|
1227
|
+
const metadataScore = (metadataSignal + sourceConfidence) * weights.metadataBoost;
|
|
1228
|
+
const modernityScore = modernMatches * weights.modernMarkerBoost - legacyMatches * weights.legacyMarkerPenalty;
|
|
1229
|
+
const exclusionPenalty = excludeMatches * weights.excludePenalty;
|
|
1230
|
+
const hasGitbookTopic = text.includes("gitbook");
|
|
1231
|
+
const hasGitSync = text.includes("git sync") || text.includes("git") && text.includes("sync");
|
|
1232
|
+
const hasApiWorkflow = text.includes("api") && (text.includes("workflow") || text.includes("sync"));
|
|
1233
|
+
const hasLegacyCli = text.includes("gitbook-cli") || text.includes("book.json");
|
|
1234
|
+
const topicScore = (hasGitbookTopic ? 3 : 0) + (hasGitSync ? 2 : 0) + (hasApiWorkflow ? 2 : 0) - (hasLegacyCli ? 4 : 0);
|
|
1235
|
+
const total = clampScore(
|
|
1236
|
+
mustHaveScore + preferScore + queryScore + starsScore + metadataScore + modernityScore + topicScore - exclusionPenalty
|
|
1237
|
+
);
|
|
1238
|
+
if (hasGitbookTopic) reasons.push({ code: "MATCH_TOPIC_GITBOOK" });
|
|
1239
|
+
if (hasGitSync) reasons.push({ code: "HAS_GIT_SYNC" });
|
|
1240
|
+
if (hasApiWorkflow) reasons.push({ code: "HAS_API_WORKFLOW" });
|
|
1241
|
+
if (hasLegacyCli) reasons.push({ code: "PENALTY_LEGACY_CLI" });
|
|
1242
|
+
if (mustHaveMatches > 0) reasons.push({ code: "MUST_HAVE_MATCH", detail: String(mustHaveMatches) });
|
|
1243
|
+
if (missingMustHave > 0) reasons.push({ code: "MISSING_MUST_HAVE", detail: String(missingMustHave) });
|
|
1244
|
+
if (preferMatches > 0) reasons.push({ code: "PREFER_MATCH", detail: String(preferMatches) });
|
|
1245
|
+
if (queryMatches > 0) reasons.push({ code: "QUERY_MATCH", detail: String(queryMatches) });
|
|
1246
|
+
if (starsSignal > 0) reasons.push({ code: "STAR_SIGNAL" });
|
|
1247
|
+
if (metadataSignal > 0) reasons.push({ code: "METADATA_SIGNAL" });
|
|
1248
|
+
if (modernMatches > 0) reasons.push({ code: "MODERN_MARKER", detail: String(modernMatches) });
|
|
1249
|
+
if (legacyMatches > 0) reasons.push({ code: "LEGACY_MARKER", detail: String(legacyMatches) });
|
|
1250
|
+
if (excludeMatches > 0) reasons.push({ code: "EXCLUDE_MATCH", detail: String(excludeMatches) });
|
|
1251
|
+
if (missingMustHave > 0) tradeoffs.push("Missing one or more required criteria terms.");
|
|
1252
|
+
if (excludeMatches > 0) tradeoffs.push("Matches one or more excluded terms.");
|
|
1253
|
+
if (skill.stars < 10) tradeoffs.push("Low quality signal from repository stars.");
|
|
1254
|
+
if (hasLegacyCli) tradeoffs.push("Contains legacy GitBook CLI markers.");
|
|
1255
|
+
const result = {
|
|
1256
|
+
skill,
|
|
1257
|
+
score: total,
|
|
1258
|
+
reasons,
|
|
1259
|
+
tradeoffs,
|
|
1260
|
+
excluded: excludeMatches > 0
|
|
1261
|
+
};
|
|
1262
|
+
if (options.includeDetails) {
|
|
1263
|
+
result.breakdown = {
|
|
1264
|
+
mustHave: clampScore(mustHaveScore),
|
|
1265
|
+
prefer: clampScore(preferScore),
|
|
1266
|
+
query: clampScore(queryScore),
|
|
1267
|
+
stars: clampScore(starsScore),
|
|
1268
|
+
metadata: clampScore(metadataScore),
|
|
1269
|
+
modernity: clampScore(modernityScore),
|
|
1270
|
+
exclusionPenalty: clampScore(exclusionPenalty),
|
|
1271
|
+
total
|
|
1272
|
+
};
|
|
1273
|
+
}
|
|
1274
|
+
return result;
|
|
1275
|
+
}
|
|
1276
|
+
function recommendSkills(skills, criteriaInput, options = {}) {
|
|
1277
|
+
const validation = validateRecommendationCriteria(criteriaInput);
|
|
1278
|
+
if (!validation.valid) {
|
|
1279
|
+
const first = validation.issues[0];
|
|
1280
|
+
const error = new Error(first?.message ?? "Invalid recommendation criteria");
|
|
1281
|
+
error.code = first?.code;
|
|
1282
|
+
error.issues = validation.issues;
|
|
1283
|
+
throw error;
|
|
1284
|
+
}
|
|
1285
|
+
const criteria = normalizeRecommendationCriteria(criteriaInput);
|
|
1286
|
+
const ranking = skills.map((skill) => scoreSkillRecommendation(skill, criteria, options)).sort((a, b) => {
|
|
1287
|
+
if (b.score !== a.score) return b.score - a.score;
|
|
1288
|
+
if (b.skill.stars !== a.skill.stars) return b.skill.stars - a.skill.stars;
|
|
1289
|
+
return a.skill.scopedName.localeCompare(b.skill.scopedName);
|
|
1290
|
+
});
|
|
1291
|
+
return {
|
|
1292
|
+
criteria,
|
|
1293
|
+
ranking: typeof options.top === "number" ? ranking.slice(0, Math.max(0, options.top)) : ranking
|
|
1294
|
+
};
|
|
1295
|
+
}
|
|
1296
|
+
var rankSkills = recommendSkills;
|
|
1297
|
+
|
|
1298
|
+
// src/core/skills/recommendation-api.ts
|
|
1299
|
+
function formatSkillRecommendations(result, opts) {
|
|
1300
|
+
const top = result.ranking;
|
|
1301
|
+
if (opts.mode === "human") {
|
|
1302
|
+
if (top.length === 0) return "No recommendations found.";
|
|
1303
|
+
const lines = ["Recommended skills:", ""];
|
|
1304
|
+
for (const [index, entry] of top.entries()) {
|
|
1305
|
+
const marker = index === 0 ? " (Recommended)" : "";
|
|
1306
|
+
lines.push(`${index + 1}) ${entry.skill.scopedName}${marker}`);
|
|
1307
|
+
lines.push(` why: ${entry.reasons.map((reason) => reason.code).join(", ") || "score-based match"}`);
|
|
1308
|
+
lines.push(` tradeoff: ${entry.tradeoffs[0] ?? "none"}`);
|
|
1309
|
+
}
|
|
1310
|
+
lines.push("");
|
|
1311
|
+
lines.push(`CHOOSE: ${top.map((_, index) => index + 1).join(",")}`);
|
|
1312
|
+
return lines.join("\n");
|
|
1313
|
+
}
|
|
1314
|
+
const options = top.map((entry, index) => ({
|
|
1315
|
+
rank: index + 1,
|
|
1316
|
+
scopedName: entry.skill.scopedName,
|
|
1317
|
+
score: entry.score,
|
|
1318
|
+
reasons: entry.reasons,
|
|
1319
|
+
tradeoffs: entry.tradeoffs,
|
|
1320
|
+
...opts.details ? {
|
|
1321
|
+
description: entry.skill.description,
|
|
1322
|
+
source: entry.skill.source,
|
|
1323
|
+
evidence: entry.breakdown ?? null
|
|
1324
|
+
} : {}
|
|
1325
|
+
}));
|
|
1326
|
+
return {
|
|
1327
|
+
query: result.criteria.query,
|
|
1328
|
+
recommended: options[0] ?? null,
|
|
1329
|
+
options
|
|
1330
|
+
};
|
|
1331
|
+
}
|
|
1332
|
+
async function searchSkills(query, options = {}) {
|
|
1333
|
+
const trimmed = query.trim();
|
|
1334
|
+
if (!trimmed) {
|
|
1335
|
+
const error = new Error("query must be non-empty");
|
|
1336
|
+
error.code = RECOMMENDATION_ERROR_CODES.QUERY_INVALID;
|
|
1337
|
+
throw error;
|
|
1338
|
+
}
|
|
1339
|
+
const client = new MarketplaceClient();
|
|
1340
|
+
try {
|
|
1341
|
+
return await client.search(trimmed, options.limit ?? 20);
|
|
1342
|
+
} catch (error) {
|
|
1343
|
+
const wrapped = new Error(error instanceof Error ? error.message : String(error));
|
|
1344
|
+
wrapped.code = RECOMMENDATION_ERROR_CODES.SOURCE_UNAVAILABLE;
|
|
1345
|
+
throw wrapped;
|
|
1346
|
+
}
|
|
1347
|
+
}
|
|
1348
|
+
async function recommendSkills2(query, criteria, options = {}) {
|
|
1349
|
+
const hits = await searchSkills(query, { limit: options.limit ?? Math.max((options.top ?? 3) * 5, 20) });
|
|
1350
|
+
const ranked = recommendSkills(hits, { ...criteria, query }, options);
|
|
1351
|
+
if (ranked.ranking.length === 0) {
|
|
1352
|
+
const error = new Error("no matches found");
|
|
1353
|
+
error.code = RECOMMENDATION_ERROR_CODES.NO_MATCHES;
|
|
1354
|
+
throw error;
|
|
1355
|
+
}
|
|
1356
|
+
return ranked;
|
|
1357
|
+
}
|
|
1358
|
+
|
|
812
1359
|
// src/core/skills/audit/scanner.ts
|
|
813
1360
|
import { readFile as readFile4 } from "fs/promises";
|
|
814
1361
|
import { existsSync as existsSync6 } from "fs";
|
|
@@ -1222,13 +1769,13 @@ async function scanFile(filePath, rules) {
|
|
|
1222
1769
|
}
|
|
1223
1770
|
async function scanDirectory(dirPath) {
|
|
1224
1771
|
const { readdir: readdir2 } = await import("fs/promises");
|
|
1225
|
-
const { join:
|
|
1772
|
+
const { join: join7 } = await import("path");
|
|
1226
1773
|
if (!existsSync6(dirPath)) return [];
|
|
1227
1774
|
const entries = await readdir2(dirPath, { withFileTypes: true });
|
|
1228
1775
|
const results = [];
|
|
1229
1776
|
for (const entry of entries) {
|
|
1230
1777
|
if (entry.isDirectory() || entry.isSymbolicLink()) {
|
|
1231
|
-
const skillFile =
|
|
1778
|
+
const skillFile = join7(dirPath, entry.name, "SKILL.md");
|
|
1232
1779
|
if (existsSync6(skillFile)) {
|
|
1233
1780
|
results.push(await scanFile(skillFile));
|
|
1234
1781
|
}
|
|
@@ -1442,9 +1989,9 @@ function getNestedValue(obj, keyPath) {
|
|
|
1442
1989
|
return current;
|
|
1443
1990
|
}
|
|
1444
1991
|
async function ensureDir(filePath) {
|
|
1445
|
-
const { mkdir:
|
|
1446
|
-
const { dirname:
|
|
1447
|
-
await
|
|
1992
|
+
const { mkdir: mkdir5 } = await import("fs/promises");
|
|
1993
|
+
const { dirname: dirname6 } = await import("path");
|
|
1994
|
+
await mkdir5(dirname6(filePath), { recursive: true });
|
|
1448
1995
|
}
|
|
1449
1996
|
|
|
1450
1997
|
// src/core/formats/json.ts
|
|
@@ -1756,14 +2303,9 @@ function getTransform(providerId) {
|
|
|
1756
2303
|
}
|
|
1757
2304
|
|
|
1758
2305
|
// src/core/mcp/reader.ts
|
|
1759
|
-
import { join as join6 } from "path";
|
|
1760
2306
|
import { existsSync as existsSync11 } from "fs";
|
|
1761
2307
|
function resolveConfigPath(provider, scope, projectDir) {
|
|
1762
|
-
|
|
1763
|
-
if (!provider.configPathProject) return null;
|
|
1764
|
-
return join6(projectDir ?? process.cwd(), provider.configPathProject);
|
|
1765
|
-
}
|
|
1766
|
-
return provider.configPathGlobal;
|
|
2308
|
+
return resolveProviderConfigPath(provider, scope, projectDir ?? process.cwd());
|
|
1767
2309
|
}
|
|
1768
2310
|
async function listMcpServers(provider, scope, projectDir) {
|
|
1769
2311
|
const configPath = resolveConfigPath(provider, scope, projectDir);
|
|
@@ -1886,37 +2428,39 @@ function buildServerConfig(source, transport, headers) {
|
|
|
1886
2428
|
|
|
1887
2429
|
// src/core/mcp/lock.ts
|
|
1888
2430
|
async function recordMcpInstall(serverName, source, sourceType, agents, isGlobal) {
|
|
1889
|
-
|
|
1890
|
-
|
|
1891
|
-
|
|
1892
|
-
|
|
1893
|
-
|
|
1894
|
-
|
|
1895
|
-
|
|
1896
|
-
|
|
1897
|
-
|
|
1898
|
-
|
|
1899
|
-
|
|
1900
|
-
|
|
1901
|
-
|
|
1902
|
-
|
|
1903
|
-
|
|
2431
|
+
await updateLockFile((lock) => {
|
|
2432
|
+
const now = (/* @__PURE__ */ new Date()).toISOString();
|
|
2433
|
+
const existing = lock.mcpServers[serverName];
|
|
2434
|
+
lock.mcpServers[serverName] = {
|
|
2435
|
+
name: serverName,
|
|
2436
|
+
scopedName: serverName,
|
|
2437
|
+
source,
|
|
2438
|
+
sourceType,
|
|
2439
|
+
installedAt: existing?.installedAt ?? now,
|
|
2440
|
+
updatedAt: now,
|
|
2441
|
+
agents: [.../* @__PURE__ */ new Set([...existing?.agents ?? [], ...agents])],
|
|
2442
|
+
canonicalPath: "",
|
|
2443
|
+
isGlobal
|
|
2444
|
+
};
|
|
2445
|
+
});
|
|
1904
2446
|
}
|
|
1905
2447
|
async function removeMcpFromLock(serverName) {
|
|
1906
|
-
|
|
1907
|
-
|
|
1908
|
-
|
|
1909
|
-
|
|
1910
|
-
|
|
2448
|
+
let removed = false;
|
|
2449
|
+
await updateLockFile((lock) => {
|
|
2450
|
+
if (!(serverName in lock.mcpServers)) return;
|
|
2451
|
+
delete lock.mcpServers[serverName];
|
|
2452
|
+
removed = true;
|
|
2453
|
+
});
|
|
2454
|
+
return removed;
|
|
1911
2455
|
}
|
|
1912
2456
|
async function getTrackedMcpServers() {
|
|
1913
2457
|
const lock = await readLockFile();
|
|
1914
2458
|
return lock.mcpServers;
|
|
1915
2459
|
}
|
|
1916
2460
|
async function saveLastSelectedAgents(agents) {
|
|
1917
|
-
|
|
1918
|
-
|
|
1919
|
-
|
|
2461
|
+
await updateLockFile((lock) => {
|
|
2462
|
+
lock.lastSelectedAgents = agents;
|
|
2463
|
+
});
|
|
1920
2464
|
}
|
|
1921
2465
|
async function getLastSelectedAgents() {
|
|
1922
2466
|
const lock = await readLockFile();
|
|
@@ -1926,7 +2470,7 @@ async function getLastSelectedAgents() {
|
|
|
1926
2470
|
// src/core/instructions/injector.ts
|
|
1927
2471
|
import { readFile as readFile9, writeFile as writeFile6 } from "fs/promises";
|
|
1928
2472
|
import { existsSync as existsSync12 } from "fs";
|
|
1929
|
-
import { join as
|
|
2473
|
+
import { join as join5, dirname as dirname4 } from "path";
|
|
1930
2474
|
import { mkdir as mkdir3 } from "fs/promises";
|
|
1931
2475
|
var MARKER_START = "<!-- CAAMP:START -->";
|
|
1932
2476
|
var MARKER_END = "<!-- CAAMP:END -->";
|
|
@@ -1956,7 +2500,7 @@ ${MARKER_END}`;
|
|
|
1956
2500
|
}
|
|
1957
2501
|
async function inject(filePath, content) {
|
|
1958
2502
|
const block = buildBlock(content);
|
|
1959
|
-
await mkdir3(
|
|
2503
|
+
await mkdir3(dirname4(filePath), { recursive: true });
|
|
1960
2504
|
if (!existsSync12(filePath)) {
|
|
1961
2505
|
await writeFile6(filePath, block + "\n", "utf-8");
|
|
1962
2506
|
return "created";
|
|
@@ -1977,8 +2521,8 @@ async function removeInjection(filePath) {
|
|
|
1977
2521
|
if (!MARKER_PATTERN.test(content)) return false;
|
|
1978
2522
|
const cleaned = content.replace(MARKER_PATTERN, "").replace(/^\n{2,}/, "\n").trim();
|
|
1979
2523
|
if (!cleaned) {
|
|
1980
|
-
const { rm:
|
|
1981
|
-
await
|
|
2524
|
+
const { rm: rm4 } = await import("fs/promises");
|
|
2525
|
+
await rm4(filePath);
|
|
1982
2526
|
} else {
|
|
1983
2527
|
await writeFile6(filePath, cleaned + "\n", "utf-8");
|
|
1984
2528
|
}
|
|
@@ -1988,7 +2532,7 @@ async function checkAllInjections(providers, projectDir, scope, expectedContent)
|
|
|
1988
2532
|
const results = [];
|
|
1989
2533
|
const checked = /* @__PURE__ */ new Set();
|
|
1990
2534
|
for (const provider of providers) {
|
|
1991
|
-
const filePath = scope === "global" ?
|
|
2535
|
+
const filePath = scope === "global" ? join5(provider.pathGlobal, provider.instructFile) : join5(projectDir, provider.instructFile);
|
|
1992
2536
|
if (checked.has(filePath)) continue;
|
|
1993
2537
|
checked.add(filePath);
|
|
1994
2538
|
const status = await checkInjection(filePath, expectedContent);
|
|
@@ -2005,7 +2549,7 @@ async function injectAll(providers, projectDir, scope, content) {
|
|
|
2005
2549
|
const results = /* @__PURE__ */ new Map();
|
|
2006
2550
|
const injected = /* @__PURE__ */ new Set();
|
|
2007
2551
|
for (const provider of providers) {
|
|
2008
|
-
const filePath = scope === "global" ?
|
|
2552
|
+
const filePath = scope === "global" ? join5(provider.pathGlobal, provider.instructFile) : join5(projectDir, provider.instructFile);
|
|
2009
2553
|
if (injected.has(filePath)) continue;
|
|
2010
2554
|
injected.add(filePath);
|
|
2011
2555
|
const action = await inject(filePath, content);
|
|
@@ -2042,7 +2586,409 @@ function groupByInstructFile(providers) {
|
|
|
2042
2586
|
return groups;
|
|
2043
2587
|
}
|
|
2044
2588
|
|
|
2589
|
+
// src/core/advanced/orchestration.ts
|
|
2590
|
+
import { existsSync as existsSync13, lstatSync as lstatSync2 } from "fs";
|
|
2591
|
+
import {
|
|
2592
|
+
cp as cp2,
|
|
2593
|
+
mkdir as mkdir4,
|
|
2594
|
+
readFile as readFile10,
|
|
2595
|
+
readlink as readlink2,
|
|
2596
|
+
rm as rm3,
|
|
2597
|
+
symlink as symlink2,
|
|
2598
|
+
writeFile as writeFile7
|
|
2599
|
+
} from "fs/promises";
|
|
2600
|
+
import { homedir as homedir2, tmpdir } from "os";
|
|
2601
|
+
import { basename as basename2, dirname as dirname5, join as join6 } from "path";
|
|
2602
|
+
var PRIORITY_ORDER = {
|
|
2603
|
+
high: 0,
|
|
2604
|
+
medium: 1,
|
|
2605
|
+
low: 2
|
|
2606
|
+
};
|
|
2607
|
+
var CANONICAL_SKILLS_DIR2 = join6(homedir2(), ".agents", "skills");
|
|
2608
|
+
function selectProvidersByMinimumPriority(providers, minimumPriority = "low") {
|
|
2609
|
+
const maxRank = PRIORITY_ORDER[minimumPriority];
|
|
2610
|
+
return [...providers].filter((provider) => PRIORITY_ORDER[provider.priority] <= maxRank).sort((a, b) => PRIORITY_ORDER[a.priority] - PRIORITY_ORDER[b.priority]);
|
|
2611
|
+
}
|
|
2612
|
+
function resolveSkillLinkPath(provider, skillName, isGlobal, projectDir) {
|
|
2613
|
+
const skillDir = isGlobal ? provider.pathSkills : join6(projectDir, provider.pathProjectSkills);
|
|
2614
|
+
return join6(skillDir, skillName);
|
|
2615
|
+
}
|
|
2616
|
+
async function snapshotConfigs(paths) {
|
|
2617
|
+
const snapshots = /* @__PURE__ */ new Map();
|
|
2618
|
+
for (const path of paths) {
|
|
2619
|
+
if (!path || snapshots.has(path)) continue;
|
|
2620
|
+
if (!existsSync13(path)) {
|
|
2621
|
+
snapshots.set(path, null);
|
|
2622
|
+
continue;
|
|
2623
|
+
}
|
|
2624
|
+
snapshots.set(path, await readFile10(path, "utf-8"));
|
|
2625
|
+
}
|
|
2626
|
+
return snapshots;
|
|
2627
|
+
}
|
|
2628
|
+
async function restoreConfigSnapshots(snapshots) {
|
|
2629
|
+
for (const [path, content] of snapshots) {
|
|
2630
|
+
if (content === null) {
|
|
2631
|
+
await rm3(path, { force: true });
|
|
2632
|
+
continue;
|
|
2633
|
+
}
|
|
2634
|
+
await mkdir4(dirname5(path), { recursive: true });
|
|
2635
|
+
await writeFile7(path, content, "utf-8");
|
|
2636
|
+
}
|
|
2637
|
+
}
|
|
2638
|
+
async function snapshotSkillState(providerTargets, operation, projectDir, backupRoot) {
|
|
2639
|
+
const skillName = operation.skillName;
|
|
2640
|
+
const isGlobal = operation.isGlobal ?? true;
|
|
2641
|
+
const canonicalPath = join6(CANONICAL_SKILLS_DIR2, skillName);
|
|
2642
|
+
const canonicalExisted = existsSync13(canonicalPath);
|
|
2643
|
+
const canonicalBackupPath = join6(backupRoot, "canonical", skillName);
|
|
2644
|
+
if (canonicalExisted) {
|
|
2645
|
+
await mkdir4(dirname5(canonicalBackupPath), { recursive: true });
|
|
2646
|
+
await cp2(canonicalPath, canonicalBackupPath, { recursive: true });
|
|
2647
|
+
}
|
|
2648
|
+
const pathSnapshots = [];
|
|
2649
|
+
for (const provider of providerTargets) {
|
|
2650
|
+
const linkPath = resolveSkillLinkPath(provider, skillName, isGlobal, projectDir);
|
|
2651
|
+
if (!existsSync13(linkPath)) {
|
|
2652
|
+
pathSnapshots.push({ linkPath, state: "missing" });
|
|
2653
|
+
continue;
|
|
2654
|
+
}
|
|
2655
|
+
const stat = lstatSync2(linkPath);
|
|
2656
|
+
if (stat.isSymbolicLink()) {
|
|
2657
|
+
pathSnapshots.push({
|
|
2658
|
+
linkPath,
|
|
2659
|
+
state: "symlink",
|
|
2660
|
+
symlinkTarget: await readlink2(linkPath)
|
|
2661
|
+
});
|
|
2662
|
+
continue;
|
|
2663
|
+
}
|
|
2664
|
+
const backupPath = join6(backupRoot, "links", provider.id, `${skillName}-${basename2(linkPath)}`);
|
|
2665
|
+
await mkdir4(dirname5(backupPath), { recursive: true });
|
|
2666
|
+
if (stat.isDirectory()) {
|
|
2667
|
+
await cp2(linkPath, backupPath, { recursive: true });
|
|
2668
|
+
pathSnapshots.push({ linkPath, state: "directory", backupPath });
|
|
2669
|
+
continue;
|
|
2670
|
+
}
|
|
2671
|
+
await cp2(linkPath, backupPath);
|
|
2672
|
+
pathSnapshots.push({ linkPath, state: "file", backupPath });
|
|
2673
|
+
}
|
|
2674
|
+
return {
|
|
2675
|
+
skillName,
|
|
2676
|
+
isGlobal,
|
|
2677
|
+
canonicalPath,
|
|
2678
|
+
canonicalBackupPath: canonicalExisted ? canonicalBackupPath : void 0,
|
|
2679
|
+
canonicalExisted,
|
|
2680
|
+
pathSnapshots
|
|
2681
|
+
};
|
|
2682
|
+
}
|
|
2683
|
+
async function restoreSkillSnapshot(snapshot) {
|
|
2684
|
+
if (existsSync13(snapshot.canonicalPath)) {
|
|
2685
|
+
await rm3(snapshot.canonicalPath, { recursive: true, force: true });
|
|
2686
|
+
}
|
|
2687
|
+
if (snapshot.canonicalExisted && snapshot.canonicalBackupPath && existsSync13(snapshot.canonicalBackupPath)) {
|
|
2688
|
+
await mkdir4(dirname5(snapshot.canonicalPath), { recursive: true });
|
|
2689
|
+
await cp2(snapshot.canonicalBackupPath, snapshot.canonicalPath, { recursive: true });
|
|
2690
|
+
}
|
|
2691
|
+
for (const pathSnapshot of snapshot.pathSnapshots) {
|
|
2692
|
+
await rm3(pathSnapshot.linkPath, { recursive: true, force: true });
|
|
2693
|
+
if (pathSnapshot.state === "missing") continue;
|
|
2694
|
+
await mkdir4(dirname5(pathSnapshot.linkPath), { recursive: true });
|
|
2695
|
+
if (pathSnapshot.state === "symlink" && pathSnapshot.symlinkTarget) {
|
|
2696
|
+
const linkType = process.platform === "win32" ? "junction" : "dir";
|
|
2697
|
+
await symlink2(pathSnapshot.symlinkTarget, pathSnapshot.linkPath, linkType);
|
|
2698
|
+
continue;
|
|
2699
|
+
}
|
|
2700
|
+
if ((pathSnapshot.state === "directory" || pathSnapshot.state === "file") && pathSnapshot.backupPath) {
|
|
2701
|
+
if (pathSnapshot.state === "directory") {
|
|
2702
|
+
await cp2(pathSnapshot.backupPath, pathSnapshot.linkPath, { recursive: true });
|
|
2703
|
+
} else {
|
|
2704
|
+
await cp2(pathSnapshot.backupPath, pathSnapshot.linkPath);
|
|
2705
|
+
}
|
|
2706
|
+
}
|
|
2707
|
+
}
|
|
2708
|
+
}
|
|
2709
|
+
async function installBatchWithRollback(options) {
|
|
2710
|
+
const projectDir = options.projectDir ?? process.cwd();
|
|
2711
|
+
const minimumPriority = options.minimumPriority ?? "low";
|
|
2712
|
+
const mcpOps = options.mcp ?? [];
|
|
2713
|
+
const skillOps = options.skills ?? [];
|
|
2714
|
+
const baseProviders = options.providers ?? getInstalledProviders();
|
|
2715
|
+
const providers = selectProvidersByMinimumPriority(baseProviders, minimumPriority);
|
|
2716
|
+
const configPaths = providers.flatMap((provider) => {
|
|
2717
|
+
const paths = [];
|
|
2718
|
+
for (const operation of mcpOps) {
|
|
2719
|
+
const path = resolveConfigPath(provider, operation.scope ?? "project", projectDir);
|
|
2720
|
+
if (path) paths.push(path);
|
|
2721
|
+
}
|
|
2722
|
+
return paths;
|
|
2723
|
+
});
|
|
2724
|
+
const configSnapshots = await snapshotConfigs(configPaths);
|
|
2725
|
+
const backupRoot = join6(
|
|
2726
|
+
tmpdir(),
|
|
2727
|
+
`caamp-skill-backup-${Date.now()}-${Math.random().toString(36).slice(2, 8)}`
|
|
2728
|
+
);
|
|
2729
|
+
const skillSnapshots = await Promise.all(
|
|
2730
|
+
skillOps.map((operation) => snapshotSkillState(providers, operation, projectDir, backupRoot))
|
|
2731
|
+
);
|
|
2732
|
+
const appliedSkills = [];
|
|
2733
|
+
const rollbackErrors = [];
|
|
2734
|
+
let mcpApplied = 0;
|
|
2735
|
+
let skillsApplied = 0;
|
|
2736
|
+
let rollbackPerformed = false;
|
|
2737
|
+
try {
|
|
2738
|
+
for (const operation of mcpOps) {
|
|
2739
|
+
const scope = operation.scope ?? "project";
|
|
2740
|
+
for (const provider of providers) {
|
|
2741
|
+
const result = await installMcpServer(
|
|
2742
|
+
provider,
|
|
2743
|
+
operation.serverName,
|
|
2744
|
+
operation.config,
|
|
2745
|
+
scope,
|
|
2746
|
+
projectDir
|
|
2747
|
+
);
|
|
2748
|
+
if (!result.success) {
|
|
2749
|
+
throw new Error(result.error ?? `Failed MCP install for ${provider.id}`);
|
|
2750
|
+
}
|
|
2751
|
+
mcpApplied += 1;
|
|
2752
|
+
}
|
|
2753
|
+
}
|
|
2754
|
+
for (const operation of skillOps) {
|
|
2755
|
+
const isGlobal = operation.isGlobal ?? true;
|
|
2756
|
+
const result = await installSkill(
|
|
2757
|
+
operation.sourcePath,
|
|
2758
|
+
operation.skillName,
|
|
2759
|
+
providers,
|
|
2760
|
+
isGlobal,
|
|
2761
|
+
projectDir
|
|
2762
|
+
);
|
|
2763
|
+
const linkedProviders = providers.filter((provider) => result.linkedAgents.includes(provider.id));
|
|
2764
|
+
appliedSkills.push({
|
|
2765
|
+
skillName: operation.skillName,
|
|
2766
|
+
isGlobal,
|
|
2767
|
+
linkedProviders
|
|
2768
|
+
});
|
|
2769
|
+
if (result.errors.length > 0) {
|
|
2770
|
+
throw new Error(result.errors.join("; "));
|
|
2771
|
+
}
|
|
2772
|
+
skillsApplied += 1;
|
|
2773
|
+
}
|
|
2774
|
+
await rm3(backupRoot, { recursive: true, force: true });
|
|
2775
|
+
return {
|
|
2776
|
+
success: true,
|
|
2777
|
+
providerIds: providers.map((provider) => provider.id),
|
|
2778
|
+
mcpApplied,
|
|
2779
|
+
skillsApplied,
|
|
2780
|
+
rollbackPerformed: false,
|
|
2781
|
+
rollbackErrors: []
|
|
2782
|
+
};
|
|
2783
|
+
} catch (error) {
|
|
2784
|
+
rollbackPerformed = true;
|
|
2785
|
+
for (const applied of [...appliedSkills].reverse()) {
|
|
2786
|
+
try {
|
|
2787
|
+
await removeSkill(applied.skillName, applied.linkedProviders, applied.isGlobal, projectDir);
|
|
2788
|
+
} catch (err) {
|
|
2789
|
+
rollbackErrors.push(err instanceof Error ? err.message : String(err));
|
|
2790
|
+
}
|
|
2791
|
+
}
|
|
2792
|
+
try {
|
|
2793
|
+
await restoreConfigSnapshots(configSnapshots);
|
|
2794
|
+
} catch (err) {
|
|
2795
|
+
rollbackErrors.push(err instanceof Error ? err.message : String(err));
|
|
2796
|
+
}
|
|
2797
|
+
for (const snapshot of skillSnapshots) {
|
|
2798
|
+
try {
|
|
2799
|
+
await restoreSkillSnapshot(snapshot);
|
|
2800
|
+
} catch (err) {
|
|
2801
|
+
rollbackErrors.push(err instanceof Error ? err.message : String(err));
|
|
2802
|
+
}
|
|
2803
|
+
}
|
|
2804
|
+
await rm3(backupRoot, { recursive: true, force: true });
|
|
2805
|
+
return {
|
|
2806
|
+
success: false,
|
|
2807
|
+
providerIds: providers.map((provider) => provider.id),
|
|
2808
|
+
mcpApplied,
|
|
2809
|
+
skillsApplied,
|
|
2810
|
+
rollbackPerformed,
|
|
2811
|
+
rollbackErrors,
|
|
2812
|
+
error: error instanceof Error ? error.message : String(error)
|
|
2813
|
+
};
|
|
2814
|
+
}
|
|
2815
|
+
}
|
|
2816
|
+
function stableStringify(value) {
|
|
2817
|
+
if (Array.isArray(value)) {
|
|
2818
|
+
return `[${value.map(stableStringify).join(",")}]`;
|
|
2819
|
+
}
|
|
2820
|
+
if (value && typeof value === "object") {
|
|
2821
|
+
const record = value;
|
|
2822
|
+
const keys = Object.keys(record).sort();
|
|
2823
|
+
return `{${keys.map((key) => `${JSON.stringify(key)}:${stableStringify(record[key])}`).join(",")}}`;
|
|
2824
|
+
}
|
|
2825
|
+
return JSON.stringify(value);
|
|
2826
|
+
}
|
|
2827
|
+
async function detectMcpConfigConflicts(providers, operations, projectDir = process.cwd()) {
|
|
2828
|
+
const conflicts = [];
|
|
2829
|
+
for (const provider of providers) {
|
|
2830
|
+
for (const operation of operations) {
|
|
2831
|
+
const scope = operation.scope ?? "project";
|
|
2832
|
+
if (operation.config.type && !provider.supportedTransports.includes(operation.config.type)) {
|
|
2833
|
+
conflicts.push({
|
|
2834
|
+
providerId: provider.id,
|
|
2835
|
+
serverName: operation.serverName,
|
|
2836
|
+
scope,
|
|
2837
|
+
code: "unsupported-transport",
|
|
2838
|
+
message: `${provider.id} does not support transport ${operation.config.type}`
|
|
2839
|
+
});
|
|
2840
|
+
}
|
|
2841
|
+
if (operation.config.headers && !provider.supportsHeaders) {
|
|
2842
|
+
conflicts.push({
|
|
2843
|
+
providerId: provider.id,
|
|
2844
|
+
serverName: operation.serverName,
|
|
2845
|
+
scope,
|
|
2846
|
+
code: "unsupported-headers",
|
|
2847
|
+
message: `${provider.id} does not support header configuration`
|
|
2848
|
+
});
|
|
2849
|
+
}
|
|
2850
|
+
const existingEntries = await listMcpServers(provider, scope, projectDir);
|
|
2851
|
+
const current = existingEntries.find((entry) => entry.name === operation.serverName);
|
|
2852
|
+
if (!current) continue;
|
|
2853
|
+
const transform = getTransform(provider.id);
|
|
2854
|
+
const desired = transform ? transform(operation.serverName, operation.config) : operation.config;
|
|
2855
|
+
if (stableStringify(current.config) !== stableStringify(desired)) {
|
|
2856
|
+
conflicts.push({
|
|
2857
|
+
providerId: provider.id,
|
|
2858
|
+
serverName: operation.serverName,
|
|
2859
|
+
scope,
|
|
2860
|
+
code: "existing-mismatch",
|
|
2861
|
+
message: `${provider.id} has existing config mismatch for ${operation.serverName}`
|
|
2862
|
+
});
|
|
2863
|
+
}
|
|
2864
|
+
}
|
|
2865
|
+
}
|
|
2866
|
+
return conflicts;
|
|
2867
|
+
}
|
|
2868
|
+
async function applyMcpInstallWithPolicy(providers, operations, policy = "fail", projectDir = process.cwd()) {
|
|
2869
|
+
const conflicts = await detectMcpConfigConflicts(providers, operations, projectDir);
|
|
2870
|
+
const conflictKey = (providerId, serverName, scope) => `${providerId}::${serverName}::${scope}`;
|
|
2871
|
+
const conflictMap = /* @__PURE__ */ new Map();
|
|
2872
|
+
for (const conflict of conflicts) {
|
|
2873
|
+
conflictMap.set(conflictKey(conflict.providerId, conflict.serverName, conflict.scope), conflict);
|
|
2874
|
+
}
|
|
2875
|
+
if (policy === "fail" && conflicts.length > 0) {
|
|
2876
|
+
return { conflicts, applied: [], skipped: [] };
|
|
2877
|
+
}
|
|
2878
|
+
const applied = [];
|
|
2879
|
+
const skipped = [];
|
|
2880
|
+
for (const provider of providers) {
|
|
2881
|
+
for (const operation of operations) {
|
|
2882
|
+
const scope = operation.scope ?? "project";
|
|
2883
|
+
const key = conflictKey(provider.id, operation.serverName, scope);
|
|
2884
|
+
const conflict = conflictMap.get(key);
|
|
2885
|
+
if (policy === "skip" && conflict) {
|
|
2886
|
+
skipped.push({
|
|
2887
|
+
providerId: provider.id,
|
|
2888
|
+
serverName: operation.serverName,
|
|
2889
|
+
scope,
|
|
2890
|
+
reason: conflict.code
|
|
2891
|
+
});
|
|
2892
|
+
continue;
|
|
2893
|
+
}
|
|
2894
|
+
const result = await installMcpServer(
|
|
2895
|
+
provider,
|
|
2896
|
+
operation.serverName,
|
|
2897
|
+
operation.config,
|
|
2898
|
+
scope,
|
|
2899
|
+
projectDir
|
|
2900
|
+
);
|
|
2901
|
+
applied.push(result);
|
|
2902
|
+
}
|
|
2903
|
+
}
|
|
2904
|
+
return { conflicts, applied, skipped };
|
|
2905
|
+
}
|
|
2906
|
+
async function updateInstructionsSingleOperation(providers, content, scope = "project", projectDir = process.cwd()) {
|
|
2907
|
+
const actions = await injectAll(providers, projectDir, scope, content);
|
|
2908
|
+
const groupedByFile = groupByInstructFile(providers);
|
|
2909
|
+
const summary = {
|
|
2910
|
+
scope,
|
|
2911
|
+
updatedFiles: actions.size,
|
|
2912
|
+
actions: []
|
|
2913
|
+
};
|
|
2914
|
+
for (const [filePath, action] of actions.entries()) {
|
|
2915
|
+
const providersForFile = providers.filter((provider) => {
|
|
2916
|
+
const expectedPath = scope === "global" ? join6(provider.pathGlobal, provider.instructFile) : join6(projectDir, provider.instructFile);
|
|
2917
|
+
return expectedPath === filePath;
|
|
2918
|
+
});
|
|
2919
|
+
const fallback = groupedByFile.get(basename2(filePath)) ?? [];
|
|
2920
|
+
const selected = providersForFile.length > 0 ? providersForFile : fallback;
|
|
2921
|
+
summary.actions.push({
|
|
2922
|
+
file: filePath,
|
|
2923
|
+
action,
|
|
2924
|
+
providers: selected.map((provider) => provider.id),
|
|
2925
|
+
configFormats: Array.from(new Set(selected.map((provider) => provider.configFormat)))
|
|
2926
|
+
});
|
|
2927
|
+
}
|
|
2928
|
+
return summary;
|
|
2929
|
+
}
|
|
2930
|
+
async function configureProviderGlobalAndProject(provider, options) {
|
|
2931
|
+
const projectDir = options.projectDir ?? process.cwd();
|
|
2932
|
+
const globalOps = options.globalMcp ?? [];
|
|
2933
|
+
const projectOps = options.projectMcp ?? [];
|
|
2934
|
+
const globalResults = [];
|
|
2935
|
+
for (const operation of globalOps) {
|
|
2936
|
+
globalResults.push(await installMcpServer(
|
|
2937
|
+
provider,
|
|
2938
|
+
operation.serverName,
|
|
2939
|
+
operation.config,
|
|
2940
|
+
"global",
|
|
2941
|
+
projectDir
|
|
2942
|
+
));
|
|
2943
|
+
}
|
|
2944
|
+
const projectResults = [];
|
|
2945
|
+
for (const operation of projectOps) {
|
|
2946
|
+
projectResults.push(await installMcpServer(
|
|
2947
|
+
provider,
|
|
2948
|
+
operation.serverName,
|
|
2949
|
+
operation.config,
|
|
2950
|
+
"project",
|
|
2951
|
+
projectDir
|
|
2952
|
+
));
|
|
2953
|
+
}
|
|
2954
|
+
const instructionResults = {};
|
|
2955
|
+
const instructionContent = options.instructionContent;
|
|
2956
|
+
if (typeof instructionContent === "string") {
|
|
2957
|
+
instructionResults.global = await injectAll([provider], projectDir, "global", instructionContent);
|
|
2958
|
+
instructionResults.project = await injectAll([provider], projectDir, "project", instructionContent);
|
|
2959
|
+
} else if (instructionContent) {
|
|
2960
|
+
if (instructionContent.global) {
|
|
2961
|
+
instructionResults.global = await injectAll([provider], projectDir, "global", instructionContent.global);
|
|
2962
|
+
}
|
|
2963
|
+
if (instructionContent.project) {
|
|
2964
|
+
instructionResults.project = await injectAll([provider], projectDir, "project", instructionContent.project);
|
|
2965
|
+
}
|
|
2966
|
+
}
|
|
2967
|
+
return {
|
|
2968
|
+
providerId: provider.id,
|
|
2969
|
+
configPaths: {
|
|
2970
|
+
global: resolveConfigPath(provider, "global", projectDir),
|
|
2971
|
+
project: resolveConfigPath(provider, "project", projectDir)
|
|
2972
|
+
},
|
|
2973
|
+
mcp: {
|
|
2974
|
+
global: globalResults,
|
|
2975
|
+
project: projectResults
|
|
2976
|
+
},
|
|
2977
|
+
instructions: instructionResults
|
|
2978
|
+
};
|
|
2979
|
+
}
|
|
2980
|
+
|
|
2045
2981
|
export {
|
|
2982
|
+
getPlatformLocations,
|
|
2983
|
+
getAgentsHome,
|
|
2984
|
+
getProjectAgentsDir,
|
|
2985
|
+
getCanonicalSkillsDir,
|
|
2986
|
+
getLockFilePath,
|
|
2987
|
+
resolveRegistryTemplatePath,
|
|
2988
|
+
resolveProviderConfigPath,
|
|
2989
|
+
resolvePreferredConfigScope,
|
|
2990
|
+
resolveProviderSkillsDir,
|
|
2991
|
+
buildSkillSubPathCandidates,
|
|
2046
2992
|
getAllProviders,
|
|
2047
2993
|
getProvider,
|
|
2048
2994
|
resolveAlias,
|
|
@@ -2060,6 +3006,7 @@ export {
|
|
|
2060
3006
|
detectAllProviders,
|
|
2061
3007
|
getInstalledProviders,
|
|
2062
3008
|
detectProjectProviders,
|
|
3009
|
+
resetDetectionCache,
|
|
2063
3010
|
parseSource,
|
|
2064
3011
|
isMarketplaceScoped,
|
|
2065
3012
|
installSkill,
|
|
@@ -2070,11 +3017,21 @@ export {
|
|
|
2070
3017
|
removeSkillFromLock,
|
|
2071
3018
|
getTrackedSkills,
|
|
2072
3019
|
checkSkillUpdate,
|
|
3020
|
+
formatNetworkError,
|
|
2073
3021
|
MarketplaceClient,
|
|
2074
3022
|
parseSkillFile,
|
|
2075
3023
|
discoverSkill,
|
|
2076
3024
|
discoverSkills,
|
|
2077
3025
|
discoverSkillsMulti,
|
|
3026
|
+
RECOMMENDATION_ERROR_CODES,
|
|
3027
|
+
tokenizeCriteriaValue,
|
|
3028
|
+
validateRecommendationCriteria,
|
|
3029
|
+
normalizeRecommendationCriteria,
|
|
3030
|
+
scoreSkillRecommendation,
|
|
3031
|
+
rankSkills,
|
|
3032
|
+
formatSkillRecommendations,
|
|
3033
|
+
searchSkills,
|
|
3034
|
+
recommendSkills2 as recommendSkills,
|
|
2078
3035
|
scanFile,
|
|
2079
3036
|
scanDirectory,
|
|
2080
3037
|
toSarif,
|
|
@@ -2104,6 +3061,12 @@ export {
|
|
|
2104
3061
|
checkAllInjections,
|
|
2105
3062
|
injectAll,
|
|
2106
3063
|
generateInjectionContent,
|
|
2107
|
-
groupByInstructFile
|
|
3064
|
+
groupByInstructFile,
|
|
3065
|
+
selectProvidersByMinimumPriority,
|
|
3066
|
+
installBatchWithRollback,
|
|
3067
|
+
detectMcpConfigConflicts,
|
|
3068
|
+
applyMcpInstallWithPolicy,
|
|
3069
|
+
updateInstructionsSingleOperation,
|
|
3070
|
+
configureProviderGlobalAndProject
|
|
2108
3071
|
};
|
|
2109
|
-
//# sourceMappingURL=chunk-
|
|
3072
|
+
//# sourceMappingURL=chunk-6HQDRJLS.js.map
|