@cleocode/caamp 0.2.0 → 0.4.0
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 +19 -2
- package/dist/{chunk-RW745KDU.js → chunk-ZYINKJDE.js} +948 -122
- package/dist/chunk-ZYINKJDE.js.map +1 -0
- package/dist/cli.js +1225 -29
- package/dist/cli.js.map +1 -1
- package/dist/index.d.ts +1764 -80
- package/dist/index.js +39 -1
- package/package.json +14 -10
- package/dist/chunk-RW745KDU.js.map +0 -1
|
@@ -140,13 +140,33 @@ function getRegistryVersion() {
|
|
|
140
140
|
return loadRegistry().version;
|
|
141
141
|
}
|
|
142
142
|
|
|
143
|
+
// src/core/logger.ts
|
|
144
|
+
var verboseMode = false;
|
|
145
|
+
var quietMode = false;
|
|
146
|
+
function setVerbose(v) {
|
|
147
|
+
verboseMode = v;
|
|
148
|
+
}
|
|
149
|
+
function setQuiet(q) {
|
|
150
|
+
quietMode = q;
|
|
151
|
+
}
|
|
152
|
+
function debug(...args) {
|
|
153
|
+
if (verboseMode) console.error("[debug]", ...args);
|
|
154
|
+
}
|
|
155
|
+
function isVerbose() {
|
|
156
|
+
return verboseMode;
|
|
157
|
+
}
|
|
158
|
+
function isQuiet() {
|
|
159
|
+
return quietMode;
|
|
160
|
+
}
|
|
161
|
+
|
|
143
162
|
// src/core/registry/detection.ts
|
|
144
163
|
import { existsSync as existsSync2 } from "fs";
|
|
145
164
|
import { execFileSync } from "child_process";
|
|
146
165
|
import { join as join2 } from "path";
|
|
147
166
|
function checkBinary(binary) {
|
|
148
167
|
try {
|
|
149
|
-
|
|
168
|
+
const cmd = process.platform === "win32" ? "where" : "which";
|
|
169
|
+
execFileSync(cmd, [binary], { stdio: "pipe" });
|
|
150
170
|
return true;
|
|
151
171
|
} catch {
|
|
152
172
|
return false;
|
|
@@ -171,10 +191,12 @@ function checkFlatpak(flatpakId) {
|
|
|
171
191
|
function detectProvider(provider) {
|
|
172
192
|
const matchedMethods = [];
|
|
173
193
|
const detection = provider.detection;
|
|
194
|
+
debug(`detecting provider ${provider.id} via methods: ${detection.methods.join(", ")}`);
|
|
174
195
|
for (const method of detection.methods) {
|
|
175
196
|
switch (method) {
|
|
176
197
|
case "binary":
|
|
177
198
|
if (detection.binary && checkBinary(detection.binary)) {
|
|
199
|
+
debug(` ${provider.id}: binary "${detection.binary}" found`);
|
|
178
200
|
matchedMethods.push("binary");
|
|
179
201
|
}
|
|
180
202
|
break;
|
|
@@ -444,7 +466,7 @@ async function listCanonicalSkills() {
|
|
|
444
466
|
return entries.filter((e) => e.isDirectory() || e.isSymbolicLink()).map((e) => e.name);
|
|
445
467
|
}
|
|
446
468
|
|
|
447
|
-
// src/core/
|
|
469
|
+
// src/core/lock-utils.ts
|
|
448
470
|
import { readFile as readFile2, writeFile as writeFile2, mkdir as mkdir2 } from "fs/promises";
|
|
449
471
|
import { existsSync as existsSync4 } from "fs";
|
|
450
472
|
import { homedir as homedir3 } from "os";
|
|
@@ -466,55 +488,9 @@ async function writeLockFile(lock) {
|
|
|
466
488
|
await mkdir2(LOCK_DIR, { recursive: true });
|
|
467
489
|
await writeFile2(LOCK_FILE, JSON.stringify(lock, null, 2) + "\n", "utf-8");
|
|
468
490
|
}
|
|
469
|
-
async function recordMcpInstall(serverName, source, sourceType, agents, isGlobal) {
|
|
470
|
-
const lock = await readLockFile();
|
|
471
|
-
const now = (/* @__PURE__ */ new Date()).toISOString();
|
|
472
|
-
const existing = lock.mcpServers[serverName];
|
|
473
|
-
lock.mcpServers[serverName] = {
|
|
474
|
-
name: serverName,
|
|
475
|
-
scopedName: serverName,
|
|
476
|
-
source,
|
|
477
|
-
sourceType,
|
|
478
|
-
installedAt: existing?.installedAt ?? now,
|
|
479
|
-
updatedAt: now,
|
|
480
|
-
agents: [.../* @__PURE__ */ new Set([...existing?.agents ?? [], ...agents])],
|
|
481
|
-
canonicalPath: "",
|
|
482
|
-
isGlobal
|
|
483
|
-
};
|
|
484
|
-
await writeLockFile(lock);
|
|
485
|
-
}
|
|
486
|
-
async function removeMcpFromLock(serverName) {
|
|
487
|
-
const lock = await readLockFile();
|
|
488
|
-
if (!(serverName in lock.mcpServers)) return false;
|
|
489
|
-
delete lock.mcpServers[serverName];
|
|
490
|
-
await writeLockFile(lock);
|
|
491
|
-
return true;
|
|
492
|
-
}
|
|
493
|
-
async function getTrackedMcpServers() {
|
|
494
|
-
const lock = await readLockFile();
|
|
495
|
-
return lock.mcpServers;
|
|
496
|
-
}
|
|
497
|
-
async function saveLastSelectedAgents(agents) {
|
|
498
|
-
const lock = await readLockFile();
|
|
499
|
-
lock.lastSelectedAgents = agents;
|
|
500
|
-
await writeLockFile(lock);
|
|
501
|
-
}
|
|
502
|
-
async function getLastSelectedAgents() {
|
|
503
|
-
const lock = await readLockFile();
|
|
504
|
-
return lock.lastSelectedAgents;
|
|
505
|
-
}
|
|
506
491
|
|
|
507
492
|
// src/core/skills/lock.ts
|
|
508
493
|
import { simpleGit } from "simple-git";
|
|
509
|
-
import { writeFile as writeFile3, mkdir as mkdir3 } from "fs/promises";
|
|
510
|
-
import { homedir as homedir4 } from "os";
|
|
511
|
-
import { join as join5 } from "path";
|
|
512
|
-
var LOCK_DIR2 = join5(homedir4(), ".agents");
|
|
513
|
-
var LOCK_FILE2 = join5(LOCK_DIR2, ".caamp-lock.json");
|
|
514
|
-
async function writeLockFile2(lock) {
|
|
515
|
-
await mkdir3(LOCK_DIR2, { recursive: true });
|
|
516
|
-
await writeFile3(LOCK_FILE2, JSON.stringify(lock, null, 2) + "\n", "utf-8");
|
|
517
|
-
}
|
|
518
494
|
async function recordSkillInstall(skillName, scopedName, source, sourceType, agents, canonicalPath, isGlobal, projectDir, version) {
|
|
519
495
|
const lock = await readLockFile();
|
|
520
496
|
const now = (/* @__PURE__ */ new Date()).toISOString();
|
|
@@ -532,13 +508,13 @@ async function recordSkillInstall(skillName, scopedName, source, sourceType, age
|
|
|
532
508
|
isGlobal,
|
|
533
509
|
projectDir
|
|
534
510
|
};
|
|
535
|
-
await
|
|
511
|
+
await writeLockFile(lock);
|
|
536
512
|
}
|
|
537
513
|
async function removeSkillFromLock(skillName) {
|
|
538
514
|
const lock = await readLockFile();
|
|
539
515
|
if (!(skillName in lock.skills)) return false;
|
|
540
516
|
delete lock.skills[skillName];
|
|
541
|
-
await
|
|
517
|
+
await writeLockFile(lock);
|
|
542
518
|
return true;
|
|
543
519
|
}
|
|
544
520
|
async function getTrackedSkills() {
|
|
@@ -600,8 +576,66 @@ async function checkSkillUpdate(skillName) {
|
|
|
600
576
|
};
|
|
601
577
|
}
|
|
602
578
|
|
|
579
|
+
// src/core/network/fetch.ts
|
|
580
|
+
var DEFAULT_FETCH_TIMEOUT_MS = 1e4;
|
|
581
|
+
var NetworkError = class extends Error {
|
|
582
|
+
kind;
|
|
583
|
+
url;
|
|
584
|
+
status;
|
|
585
|
+
constructor(message, kind, url, status) {
|
|
586
|
+
super(message);
|
|
587
|
+
this.name = "NetworkError";
|
|
588
|
+
this.kind = kind;
|
|
589
|
+
this.url = url;
|
|
590
|
+
this.status = status;
|
|
591
|
+
}
|
|
592
|
+
};
|
|
593
|
+
function isAbortError(error) {
|
|
594
|
+
return error instanceof Error && error.name === "AbortError";
|
|
595
|
+
}
|
|
596
|
+
async function fetchWithTimeout(url, init, timeoutMs = DEFAULT_FETCH_TIMEOUT_MS) {
|
|
597
|
+
try {
|
|
598
|
+
return await fetch(url, {
|
|
599
|
+
...init,
|
|
600
|
+
signal: AbortSignal.timeout(timeoutMs)
|
|
601
|
+
});
|
|
602
|
+
} catch (error) {
|
|
603
|
+
if (isAbortError(error)) {
|
|
604
|
+
throw new NetworkError(`Request timed out after ${timeoutMs}ms`, "timeout", url);
|
|
605
|
+
}
|
|
606
|
+
throw new NetworkError("Network request failed", "network", url);
|
|
607
|
+
}
|
|
608
|
+
}
|
|
609
|
+
function ensureOkResponse(response, url) {
|
|
610
|
+
if (!response.ok) {
|
|
611
|
+
throw new NetworkError(`Request failed with status ${response.status}`, "http", url, response.status);
|
|
612
|
+
}
|
|
613
|
+
return response;
|
|
614
|
+
}
|
|
615
|
+
function formatNetworkError(error) {
|
|
616
|
+
if (error instanceof NetworkError) {
|
|
617
|
+
if (error.kind === "timeout") {
|
|
618
|
+
return "Network request timed out. Please check your connection and try again.";
|
|
619
|
+
}
|
|
620
|
+
if (error.kind === "http") {
|
|
621
|
+
return `Marketplace request failed with HTTP ${error.status ?? "unknown"}. Please try again shortly.`;
|
|
622
|
+
}
|
|
623
|
+
return "Network request failed. Please check your connection and try again.";
|
|
624
|
+
}
|
|
625
|
+
if (error instanceof Error) return error.message;
|
|
626
|
+
return String(error);
|
|
627
|
+
}
|
|
628
|
+
|
|
603
629
|
// src/core/marketplace/skillsmp.ts
|
|
604
630
|
var API_BASE = "https://www.agentskills.in/api/skills";
|
|
631
|
+
function parseScopedName(value) {
|
|
632
|
+
const match = value.match(/^@([^/]+)\/([^/]+)$/);
|
|
633
|
+
if (!match) return null;
|
|
634
|
+
return {
|
|
635
|
+
author: match[1],
|
|
636
|
+
name: match[2]
|
|
637
|
+
};
|
|
638
|
+
}
|
|
605
639
|
function toResult(skill) {
|
|
606
640
|
return {
|
|
607
641
|
name: skill.name,
|
|
@@ -623,31 +657,34 @@ var SkillsMPAdapter = class {
|
|
|
623
657
|
limit: String(limit),
|
|
624
658
|
sortBy: "stars"
|
|
625
659
|
});
|
|
626
|
-
|
|
627
|
-
|
|
628
|
-
|
|
629
|
-
|
|
630
|
-
return data.skills.map(toResult);
|
|
631
|
-
} catch {
|
|
632
|
-
return [];
|
|
633
|
-
}
|
|
660
|
+
const url = `${API_BASE}?${params}`;
|
|
661
|
+
const response = ensureOkResponse(await fetchWithTimeout(url), url);
|
|
662
|
+
const data = await response.json();
|
|
663
|
+
return data.skills.map(toResult);
|
|
634
664
|
}
|
|
635
665
|
async getSkill(scopedName) {
|
|
636
|
-
const
|
|
637
|
-
|
|
638
|
-
|
|
639
|
-
|
|
640
|
-
|
|
641
|
-
|
|
642
|
-
|
|
666
|
+
const parts = parseScopedName(scopedName);
|
|
667
|
+
const searchTerms = parts ? [parts.name, `${parts.author} ${parts.name}`, scopedName] : [scopedName];
|
|
668
|
+
const seen = /* @__PURE__ */ new Set();
|
|
669
|
+
for (const term of searchTerms) {
|
|
670
|
+
if (seen.has(term)) continue;
|
|
671
|
+
seen.add(term);
|
|
672
|
+
const params = new URLSearchParams({
|
|
673
|
+
search: term,
|
|
674
|
+
limit: "50",
|
|
675
|
+
sortBy: "stars"
|
|
676
|
+
});
|
|
677
|
+
const url = `${API_BASE}?${params}`;
|
|
678
|
+
const response = ensureOkResponse(await fetchWithTimeout(url), url);
|
|
643
679
|
const data = await response.json();
|
|
644
680
|
const match = data.skills.find(
|
|
645
681
|
(s) => s.scopedName === scopedName || `@${s.author}/${s.name}` === scopedName
|
|
646
682
|
);
|
|
647
|
-
|
|
648
|
-
|
|
649
|
-
|
|
683
|
+
if (match) {
|
|
684
|
+
return toResult(match);
|
|
685
|
+
}
|
|
650
686
|
}
|
|
687
|
+
return null;
|
|
651
688
|
}
|
|
652
689
|
};
|
|
653
690
|
|
|
@@ -669,18 +706,14 @@ function toResult2(skill) {
|
|
|
669
706
|
var SkillsShAdapter = class {
|
|
670
707
|
name = "skills.sh";
|
|
671
708
|
async search(query, limit = 20) {
|
|
672
|
-
|
|
673
|
-
|
|
674
|
-
|
|
675
|
-
|
|
676
|
-
|
|
677
|
-
|
|
678
|
-
|
|
679
|
-
|
|
680
|
-
return data.results.map(toResult2);
|
|
681
|
-
} catch {
|
|
682
|
-
return [];
|
|
683
|
-
}
|
|
709
|
+
const params = new URLSearchParams({
|
|
710
|
+
q: query,
|
|
711
|
+
limit: String(limit)
|
|
712
|
+
});
|
|
713
|
+
const url = `${API_BASE2}/search?${params}`;
|
|
714
|
+
const response = ensureOkResponse(await fetchWithTimeout(url), url);
|
|
715
|
+
const data = await response.json();
|
|
716
|
+
return data.results.map(toResult2);
|
|
684
717
|
}
|
|
685
718
|
async getSkill(scopedName) {
|
|
686
719
|
const results = await this.search(scopedName, 5);
|
|
@@ -689,21 +722,68 @@ var SkillsShAdapter = class {
|
|
|
689
722
|
};
|
|
690
723
|
|
|
691
724
|
// src/core/marketplace/client.ts
|
|
725
|
+
var MarketplaceUnavailableError = class extends Error {
|
|
726
|
+
details;
|
|
727
|
+
constructor(message, details) {
|
|
728
|
+
super(message);
|
|
729
|
+
this.name = "MarketplaceUnavailableError";
|
|
730
|
+
this.details = details;
|
|
731
|
+
}
|
|
732
|
+
};
|
|
692
733
|
var MarketplaceClient = class {
|
|
693
734
|
adapters;
|
|
735
|
+
/**
|
|
736
|
+
* Create a new marketplace client.
|
|
737
|
+
*
|
|
738
|
+
* @param adapters - Custom marketplace adapters (defaults to agentskills.in and skills.sh)
|
|
739
|
+
*
|
|
740
|
+
* @example
|
|
741
|
+
* ```typescript
|
|
742
|
+
* // Use default adapters
|
|
743
|
+
* const client = new MarketplaceClient();
|
|
744
|
+
*
|
|
745
|
+
* // Use custom adapters
|
|
746
|
+
* const client = new MarketplaceClient([myAdapter]);
|
|
747
|
+
* ```
|
|
748
|
+
*/
|
|
694
749
|
constructor(adapters) {
|
|
695
750
|
this.adapters = adapters ?? [
|
|
696
751
|
new SkillsMPAdapter(),
|
|
697
752
|
new SkillsShAdapter()
|
|
698
753
|
];
|
|
699
754
|
}
|
|
700
|
-
/**
|
|
755
|
+
/**
|
|
756
|
+
* Search all marketplaces and return deduplicated, sorted results.
|
|
757
|
+
*
|
|
758
|
+
* Queries all adapters in parallel and deduplicates by `scopedName`,
|
|
759
|
+
* keeping the entry with the highest star count. Results are sorted by
|
|
760
|
+
* stars descending.
|
|
761
|
+
*
|
|
762
|
+
* @param query - Search query string
|
|
763
|
+
* @param limit - Maximum number of results to return (default: 20)
|
|
764
|
+
* @returns Deduplicated and sorted marketplace results
|
|
765
|
+
*
|
|
766
|
+
* @example
|
|
767
|
+
* ```typescript
|
|
768
|
+
* const results = await client.search("code review", 10);
|
|
769
|
+
* ```
|
|
770
|
+
*/
|
|
701
771
|
async search(query, limit = 20) {
|
|
702
|
-
const
|
|
703
|
-
|
|
704
|
-
|
|
705
|
-
const
|
|
706
|
-
|
|
772
|
+
const settled = await Promise.allSettled(this.adapters.map((adapter) => adapter.search(query, limit)));
|
|
773
|
+
const flat = [];
|
|
774
|
+
const failures = [];
|
|
775
|
+
for (const [index, result] of settled.entries()) {
|
|
776
|
+
const adapterName = this.adapters[index]?.name ?? "unknown";
|
|
777
|
+
if (result.status === "fulfilled") {
|
|
778
|
+
flat.push(...result.value);
|
|
779
|
+
} else {
|
|
780
|
+
const reason = result.reason instanceof Error ? result.reason.message : String(result.reason);
|
|
781
|
+
failures.push(`${adapterName}: ${reason}`);
|
|
782
|
+
}
|
|
783
|
+
}
|
|
784
|
+
if (flat.length === 0 && failures.length > 0) {
|
|
785
|
+
throw new MarketplaceUnavailableError("All marketplace sources failed.", failures);
|
|
786
|
+
}
|
|
707
787
|
const seen = /* @__PURE__ */ new Map();
|
|
708
788
|
for (const result of flat) {
|
|
709
789
|
const existing = seen.get(result.scopedName);
|
|
@@ -715,11 +795,32 @@ var MarketplaceClient = class {
|
|
|
715
795
|
deduplicated.sort((a, b) => b.stars - a.stars);
|
|
716
796
|
return deduplicated.slice(0, limit);
|
|
717
797
|
}
|
|
718
|
-
/**
|
|
798
|
+
/**
|
|
799
|
+
* Get a specific skill by its scoped name from any marketplace.
|
|
800
|
+
*
|
|
801
|
+
* Tries each adapter in order and returns the first match.
|
|
802
|
+
*
|
|
803
|
+
* @param scopedName - Scoped skill name (e.g. `"@author/my-skill"`)
|
|
804
|
+
* @returns The marketplace result, or `null` if not found in any marketplace
|
|
805
|
+
*
|
|
806
|
+
* @example
|
|
807
|
+
* ```typescript
|
|
808
|
+
* const skill = await client.getSkill("@anthropic/memory");
|
|
809
|
+
* ```
|
|
810
|
+
*/
|
|
719
811
|
async getSkill(scopedName) {
|
|
812
|
+
const failures = [];
|
|
720
813
|
for (const adapter of this.adapters) {
|
|
721
|
-
|
|
722
|
-
|
|
814
|
+
try {
|
|
815
|
+
const result = await adapter.getSkill(scopedName);
|
|
816
|
+
if (result) return result;
|
|
817
|
+
} catch (error) {
|
|
818
|
+
const reason = error instanceof Error ? error.message : String(error);
|
|
819
|
+
failures.push(`${adapter.name}: ${reason}`);
|
|
820
|
+
}
|
|
821
|
+
}
|
|
822
|
+
if (failures.length === this.adapters.length && this.adapters.length > 0) {
|
|
823
|
+
throw new MarketplaceUnavailableError("All marketplace sources failed.", failures);
|
|
723
824
|
}
|
|
724
825
|
return null;
|
|
725
826
|
}
|
|
@@ -728,7 +829,7 @@ var MarketplaceClient = class {
|
|
|
728
829
|
// src/core/skills/discovery.ts
|
|
729
830
|
import { readFile as readFile3, readdir } from "fs/promises";
|
|
730
831
|
import { existsSync as existsSync5 } from "fs";
|
|
731
|
-
import { join as
|
|
832
|
+
import { join as join5 } from "path";
|
|
732
833
|
import matter from "gray-matter";
|
|
733
834
|
async function parseSkillFile(filePath) {
|
|
734
835
|
try {
|
|
@@ -752,7 +853,7 @@ async function parseSkillFile(filePath) {
|
|
|
752
853
|
}
|
|
753
854
|
}
|
|
754
855
|
async function discoverSkill(skillDir) {
|
|
755
|
-
const skillFile =
|
|
856
|
+
const skillFile = join5(skillDir, "SKILL.md");
|
|
756
857
|
if (!existsSync5(skillFile)) return null;
|
|
757
858
|
const metadata = await parseSkillFile(skillFile);
|
|
758
859
|
if (!metadata) return null;
|
|
@@ -769,7 +870,7 @@ async function discoverSkills(rootDir) {
|
|
|
769
870
|
const skills = [];
|
|
770
871
|
for (const entry of entries) {
|
|
771
872
|
if (!entry.isDirectory() && !entry.isSymbolicLink()) continue;
|
|
772
|
-
const skillDir =
|
|
873
|
+
const skillDir = join5(rootDir, entry.name);
|
|
773
874
|
const skill = await discoverSkill(skillDir);
|
|
774
875
|
if (skill) {
|
|
775
876
|
skills.push(skill);
|
|
@@ -792,6 +893,273 @@ async function discoverSkillsMulti(dirs) {
|
|
|
792
893
|
return all;
|
|
793
894
|
}
|
|
794
895
|
|
|
896
|
+
// src/core/skills/recommendation.ts
|
|
897
|
+
var RECOMMENDATION_ERROR_CODES = {
|
|
898
|
+
QUERY_INVALID: "E_SKILLS_QUERY_INVALID",
|
|
899
|
+
NO_MATCHES: "E_SKILLS_NO_MATCHES",
|
|
900
|
+
SOURCE_UNAVAILABLE: "E_SKILLS_SOURCE_UNAVAILABLE",
|
|
901
|
+
CRITERIA_CONFLICT: "E_SKILLS_CRITERIA_CONFLICT"
|
|
902
|
+
};
|
|
903
|
+
var DEFAULT_WEIGHTS = {
|
|
904
|
+
mustHaveMatch: 10,
|
|
905
|
+
preferMatch: 4,
|
|
906
|
+
queryTokenMatch: 3,
|
|
907
|
+
starsFactor: 2,
|
|
908
|
+
metadataBoost: 2,
|
|
909
|
+
modernMarkerBoost: 3,
|
|
910
|
+
legacyMarkerPenalty: 3,
|
|
911
|
+
excludePenalty: 25,
|
|
912
|
+
missingMustHavePenalty: 20
|
|
913
|
+
};
|
|
914
|
+
var DEFAULT_MODERN_MARKERS = ["svelte 5", "runes", "lafs", "slsa", "drizzle", "better-auth"];
|
|
915
|
+
var DEFAULT_LEGACY_MARKERS = ["svelte 3", "jquery", "bower", "legacy", "book.json", "gitbook-cli"];
|
|
916
|
+
function tokenizeCriteriaValue(value) {
|
|
917
|
+
return value.split(",").map((part) => part.trim().toLowerCase()).filter(Boolean);
|
|
918
|
+
}
|
|
919
|
+
function normalizeList(value) {
|
|
920
|
+
if (value === void 0) return [];
|
|
921
|
+
if (!(typeof value === "string" || Array.isArray(value))) return [];
|
|
922
|
+
const source = Array.isArray(value) ? value : [value];
|
|
923
|
+
const flattened = source.flatMap((item) => typeof item === "string" ? tokenizeCriteriaValue(item) : []);
|
|
924
|
+
return Array.from(new Set(flattened)).sort((a, b) => a.localeCompare(b));
|
|
925
|
+
}
|
|
926
|
+
function hasAnyCriteriaInput(input) {
|
|
927
|
+
const query = typeof input.query === "string" ? input.query.trim() : "";
|
|
928
|
+
if (query.length > 0) return true;
|
|
929
|
+
const lists = [input.mustHave, input.prefer, input.exclude];
|
|
930
|
+
return lists.some((list) => normalizeList(list).length > 0);
|
|
931
|
+
}
|
|
932
|
+
function validateRecommendationCriteria(input) {
|
|
933
|
+
const issues = [];
|
|
934
|
+
if (input.query !== void 0 && typeof input.query !== "string") {
|
|
935
|
+
issues.push({
|
|
936
|
+
code: RECOMMENDATION_ERROR_CODES.QUERY_INVALID,
|
|
937
|
+
field: "query",
|
|
938
|
+
message: "query must be a string"
|
|
939
|
+
});
|
|
940
|
+
}
|
|
941
|
+
if (input.mustHave !== void 0 && !(typeof input.mustHave === "string" || Array.isArray(input.mustHave))) {
|
|
942
|
+
issues.push({
|
|
943
|
+
code: RECOMMENDATION_ERROR_CODES.QUERY_INVALID,
|
|
944
|
+
field: "mustHave",
|
|
945
|
+
message: "mustHave must be a string or string[]"
|
|
946
|
+
});
|
|
947
|
+
}
|
|
948
|
+
if (input.prefer !== void 0 && !(typeof input.prefer === "string" || Array.isArray(input.prefer))) {
|
|
949
|
+
issues.push({
|
|
950
|
+
code: RECOMMENDATION_ERROR_CODES.QUERY_INVALID,
|
|
951
|
+
field: "prefer",
|
|
952
|
+
message: "prefer must be a string or string[]"
|
|
953
|
+
});
|
|
954
|
+
}
|
|
955
|
+
if (input.exclude !== void 0 && !(typeof input.exclude === "string" || Array.isArray(input.exclude))) {
|
|
956
|
+
issues.push({
|
|
957
|
+
code: RECOMMENDATION_ERROR_CODES.QUERY_INVALID,
|
|
958
|
+
field: "exclude",
|
|
959
|
+
message: "exclude must be a string or string[]"
|
|
960
|
+
});
|
|
961
|
+
}
|
|
962
|
+
const mustHave = normalizeList(input.mustHave);
|
|
963
|
+
const prefer = normalizeList(input.prefer);
|
|
964
|
+
const exclude = normalizeList(input.exclude);
|
|
965
|
+
const conflict = mustHave.some((term) => exclude.includes(term)) || prefer.some((term) => exclude.includes(term));
|
|
966
|
+
if (conflict) {
|
|
967
|
+
issues.push({
|
|
968
|
+
code: RECOMMENDATION_ERROR_CODES.CRITERIA_CONFLICT,
|
|
969
|
+
field: "exclude",
|
|
970
|
+
message: "criteria terms cannot appear in both prefer/must-have and exclude"
|
|
971
|
+
});
|
|
972
|
+
}
|
|
973
|
+
if (issues.length === 0 && !hasAnyCriteriaInput(input)) {
|
|
974
|
+
issues.push({
|
|
975
|
+
code: RECOMMENDATION_ERROR_CODES.QUERY_INVALID,
|
|
976
|
+
field: "query",
|
|
977
|
+
message: "at least one criteria value is required"
|
|
978
|
+
});
|
|
979
|
+
}
|
|
980
|
+
return {
|
|
981
|
+
valid: issues.length === 0,
|
|
982
|
+
issues
|
|
983
|
+
};
|
|
984
|
+
}
|
|
985
|
+
function normalizeRecommendationCriteria(input) {
|
|
986
|
+
const query = (input.query ?? "").trim().toLowerCase();
|
|
987
|
+
return {
|
|
988
|
+
query,
|
|
989
|
+
queryTokens: query ? Array.from(new Set(tokenizeCriteriaValue(query.replace(/\s+/g, ",")))).sort((a, b) => a.localeCompare(b)) : [],
|
|
990
|
+
mustHave: normalizeList(input.mustHave),
|
|
991
|
+
prefer: normalizeList(input.prefer),
|
|
992
|
+
exclude: normalizeList(input.exclude)
|
|
993
|
+
};
|
|
994
|
+
}
|
|
995
|
+
function countMatches(haystack, needles) {
|
|
996
|
+
let count = 0;
|
|
997
|
+
for (const needle of needles) {
|
|
998
|
+
if (haystack.includes(needle)) {
|
|
999
|
+
count += 1;
|
|
1000
|
+
}
|
|
1001
|
+
}
|
|
1002
|
+
return count;
|
|
1003
|
+
}
|
|
1004
|
+
function clampScore(value) {
|
|
1005
|
+
return Number(value.toFixed(6));
|
|
1006
|
+
}
|
|
1007
|
+
function buildSearchText(skill) {
|
|
1008
|
+
return `${skill.name} ${skill.scopedName} ${skill.description} ${skill.author}`.toLowerCase();
|
|
1009
|
+
}
|
|
1010
|
+
function scoreSkillRecommendation(skill, criteria, options = {}) {
|
|
1011
|
+
const weights = { ...DEFAULT_WEIGHTS, ...options.weights };
|
|
1012
|
+
const modernMarkers = (options.modernMarkers ?? DEFAULT_MODERN_MARKERS).map((marker) => marker.toLowerCase());
|
|
1013
|
+
const legacyMarkers = (options.legacyMarkers ?? DEFAULT_LEGACY_MARKERS).map((marker) => marker.toLowerCase());
|
|
1014
|
+
const text = buildSearchText(skill);
|
|
1015
|
+
const reasons = [];
|
|
1016
|
+
const tradeoffs = [];
|
|
1017
|
+
const mustHaveMatches = countMatches(text, criteria.mustHave);
|
|
1018
|
+
const missingMustHave = Math.max(criteria.mustHave.length - mustHaveMatches, 0);
|
|
1019
|
+
const preferMatches = countMatches(text, criteria.prefer);
|
|
1020
|
+
const queryMatches = countMatches(text, criteria.queryTokens);
|
|
1021
|
+
const excludeMatches = countMatches(text, criteria.exclude);
|
|
1022
|
+
const modernMatches = countMatches(text, modernMarkers);
|
|
1023
|
+
const legacyMatches = countMatches(text, legacyMarkers);
|
|
1024
|
+
const metadataSignal = skill.description.trim().length >= 80 ? 1 : 0;
|
|
1025
|
+
const starsSignal = Math.log10(skill.stars + 1);
|
|
1026
|
+
const sourceConfidence = skill.source === "agentskills.in" ? 1 : skill.source === "skills.sh" ? 0.8 : 0.6;
|
|
1027
|
+
const mustHaveScore = mustHaveMatches * weights.mustHaveMatch - missingMustHave * weights.missingMustHavePenalty;
|
|
1028
|
+
const preferScore = preferMatches * weights.preferMatch;
|
|
1029
|
+
const queryScore = queryMatches * weights.queryTokenMatch;
|
|
1030
|
+
const starsScore = starsSignal * weights.starsFactor;
|
|
1031
|
+
const metadataScore = (metadataSignal + sourceConfidence) * weights.metadataBoost;
|
|
1032
|
+
const modernityScore = modernMatches * weights.modernMarkerBoost - legacyMatches * weights.legacyMarkerPenalty;
|
|
1033
|
+
const exclusionPenalty = excludeMatches * weights.excludePenalty;
|
|
1034
|
+
const hasGitbookTopic = text.includes("gitbook");
|
|
1035
|
+
const hasGitSync = text.includes("git sync") || text.includes("git") && text.includes("sync");
|
|
1036
|
+
const hasApiWorkflow = text.includes("api") && (text.includes("workflow") || text.includes("sync"));
|
|
1037
|
+
const hasLegacyCli = text.includes("gitbook-cli") || text.includes("book.json");
|
|
1038
|
+
const topicScore = (hasGitbookTopic ? 3 : 0) + (hasGitSync ? 2 : 0) + (hasApiWorkflow ? 2 : 0) - (hasLegacyCli ? 4 : 0);
|
|
1039
|
+
const total = clampScore(
|
|
1040
|
+
mustHaveScore + preferScore + queryScore + starsScore + metadataScore + modernityScore + topicScore - exclusionPenalty
|
|
1041
|
+
);
|
|
1042
|
+
if (hasGitbookTopic) reasons.push({ code: "MATCH_TOPIC_GITBOOK" });
|
|
1043
|
+
if (hasGitSync) reasons.push({ code: "HAS_GIT_SYNC" });
|
|
1044
|
+
if (hasApiWorkflow) reasons.push({ code: "HAS_API_WORKFLOW" });
|
|
1045
|
+
if (hasLegacyCli) reasons.push({ code: "PENALTY_LEGACY_CLI" });
|
|
1046
|
+
if (mustHaveMatches > 0) reasons.push({ code: "MUST_HAVE_MATCH", detail: String(mustHaveMatches) });
|
|
1047
|
+
if (missingMustHave > 0) reasons.push({ code: "MISSING_MUST_HAVE", detail: String(missingMustHave) });
|
|
1048
|
+
if (preferMatches > 0) reasons.push({ code: "PREFER_MATCH", detail: String(preferMatches) });
|
|
1049
|
+
if (queryMatches > 0) reasons.push({ code: "QUERY_MATCH", detail: String(queryMatches) });
|
|
1050
|
+
if (starsSignal > 0) reasons.push({ code: "STAR_SIGNAL" });
|
|
1051
|
+
if (metadataSignal > 0) reasons.push({ code: "METADATA_SIGNAL" });
|
|
1052
|
+
if (modernMatches > 0) reasons.push({ code: "MODERN_MARKER", detail: String(modernMatches) });
|
|
1053
|
+
if (legacyMatches > 0) reasons.push({ code: "LEGACY_MARKER", detail: String(legacyMatches) });
|
|
1054
|
+
if (excludeMatches > 0) reasons.push({ code: "EXCLUDE_MATCH", detail: String(excludeMatches) });
|
|
1055
|
+
if (missingMustHave > 0) tradeoffs.push("Missing one or more required criteria terms.");
|
|
1056
|
+
if (excludeMatches > 0) tradeoffs.push("Matches one or more excluded terms.");
|
|
1057
|
+
if (skill.stars < 10) tradeoffs.push("Low quality signal from repository stars.");
|
|
1058
|
+
if (hasLegacyCli) tradeoffs.push("Contains legacy GitBook CLI markers.");
|
|
1059
|
+
const result = {
|
|
1060
|
+
skill,
|
|
1061
|
+
score: total,
|
|
1062
|
+
reasons,
|
|
1063
|
+
tradeoffs,
|
|
1064
|
+
excluded: excludeMatches > 0
|
|
1065
|
+
};
|
|
1066
|
+
if (options.includeDetails) {
|
|
1067
|
+
result.breakdown = {
|
|
1068
|
+
mustHave: clampScore(mustHaveScore),
|
|
1069
|
+
prefer: clampScore(preferScore),
|
|
1070
|
+
query: clampScore(queryScore),
|
|
1071
|
+
stars: clampScore(starsScore),
|
|
1072
|
+
metadata: clampScore(metadataScore),
|
|
1073
|
+
modernity: clampScore(modernityScore),
|
|
1074
|
+
exclusionPenalty: clampScore(exclusionPenalty),
|
|
1075
|
+
total
|
|
1076
|
+
};
|
|
1077
|
+
}
|
|
1078
|
+
return result;
|
|
1079
|
+
}
|
|
1080
|
+
function recommendSkills(skills, criteriaInput, options = {}) {
|
|
1081
|
+
const validation = validateRecommendationCriteria(criteriaInput);
|
|
1082
|
+
if (!validation.valid) {
|
|
1083
|
+
const first = validation.issues[0];
|
|
1084
|
+
const error = new Error(first?.message ?? "Invalid recommendation criteria");
|
|
1085
|
+
error.code = first?.code;
|
|
1086
|
+
error.issues = validation.issues;
|
|
1087
|
+
throw error;
|
|
1088
|
+
}
|
|
1089
|
+
const criteria = normalizeRecommendationCriteria(criteriaInput);
|
|
1090
|
+
const ranking = skills.map((skill) => scoreSkillRecommendation(skill, criteria, options)).sort((a, b) => {
|
|
1091
|
+
if (b.score !== a.score) return b.score - a.score;
|
|
1092
|
+
if (b.skill.stars !== a.skill.stars) return b.skill.stars - a.skill.stars;
|
|
1093
|
+
return a.skill.scopedName.localeCompare(b.skill.scopedName);
|
|
1094
|
+
});
|
|
1095
|
+
return {
|
|
1096
|
+
criteria,
|
|
1097
|
+
ranking: typeof options.top === "number" ? ranking.slice(0, Math.max(0, options.top)) : ranking
|
|
1098
|
+
};
|
|
1099
|
+
}
|
|
1100
|
+
var rankSkills = recommendSkills;
|
|
1101
|
+
|
|
1102
|
+
// src/core/skills/recommendation-api.ts
|
|
1103
|
+
function formatSkillRecommendations(result, opts) {
|
|
1104
|
+
const top = result.ranking;
|
|
1105
|
+
if (opts.mode === "human") {
|
|
1106
|
+
if (top.length === 0) return "No recommendations found.";
|
|
1107
|
+
const lines = ["Recommended skills:", ""];
|
|
1108
|
+
for (const [index, entry] of top.entries()) {
|
|
1109
|
+
const marker = index === 0 ? " (Recommended)" : "";
|
|
1110
|
+
lines.push(`${index + 1}) ${entry.skill.scopedName}${marker}`);
|
|
1111
|
+
lines.push(` why: ${entry.reasons.map((reason) => reason.code).join(", ") || "score-based match"}`);
|
|
1112
|
+
lines.push(` tradeoff: ${entry.tradeoffs[0] ?? "none"}`);
|
|
1113
|
+
}
|
|
1114
|
+
lines.push("");
|
|
1115
|
+
lines.push(`CHOOSE: ${top.map((_, index) => index + 1).join(",")}`);
|
|
1116
|
+
return lines.join("\n");
|
|
1117
|
+
}
|
|
1118
|
+
const options = top.map((entry, index) => ({
|
|
1119
|
+
rank: index + 1,
|
|
1120
|
+
scopedName: entry.skill.scopedName,
|
|
1121
|
+
score: entry.score,
|
|
1122
|
+
reasons: entry.reasons,
|
|
1123
|
+
tradeoffs: entry.tradeoffs,
|
|
1124
|
+
...opts.details ? {
|
|
1125
|
+
description: entry.skill.description,
|
|
1126
|
+
source: entry.skill.source,
|
|
1127
|
+
evidence: entry.breakdown ?? null
|
|
1128
|
+
} : {}
|
|
1129
|
+
}));
|
|
1130
|
+
return {
|
|
1131
|
+
query: result.criteria.query,
|
|
1132
|
+
recommended: options[0] ?? null,
|
|
1133
|
+
options
|
|
1134
|
+
};
|
|
1135
|
+
}
|
|
1136
|
+
async function searchSkills(query, options = {}) {
|
|
1137
|
+
const trimmed = query.trim();
|
|
1138
|
+
if (!trimmed) {
|
|
1139
|
+
const error = new Error("query must be non-empty");
|
|
1140
|
+
error.code = RECOMMENDATION_ERROR_CODES.QUERY_INVALID;
|
|
1141
|
+
throw error;
|
|
1142
|
+
}
|
|
1143
|
+
const client = new MarketplaceClient();
|
|
1144
|
+
try {
|
|
1145
|
+
return await client.search(trimmed, options.limit ?? 20);
|
|
1146
|
+
} catch (error) {
|
|
1147
|
+
const wrapped = new Error(error instanceof Error ? error.message : String(error));
|
|
1148
|
+
wrapped.code = RECOMMENDATION_ERROR_CODES.SOURCE_UNAVAILABLE;
|
|
1149
|
+
throw wrapped;
|
|
1150
|
+
}
|
|
1151
|
+
}
|
|
1152
|
+
async function recommendSkills2(query, criteria, options = {}) {
|
|
1153
|
+
const hits = await searchSkills(query, { limit: options.limit ?? Math.max((options.top ?? 3) * 5, 20) });
|
|
1154
|
+
const ranked = recommendSkills(hits, { ...criteria, query }, options);
|
|
1155
|
+
if (ranked.ranking.length === 0) {
|
|
1156
|
+
const error = new Error("no matches found");
|
|
1157
|
+
error.code = RECOMMENDATION_ERROR_CODES.NO_MATCHES;
|
|
1158
|
+
throw error;
|
|
1159
|
+
}
|
|
1160
|
+
return ranked;
|
|
1161
|
+
}
|
|
1162
|
+
|
|
795
1163
|
// src/core/skills/audit/scanner.ts
|
|
796
1164
|
import { readFile as readFile4 } from "fs/promises";
|
|
797
1165
|
import { existsSync as existsSync6 } from "fs";
|
|
@@ -1431,7 +1799,7 @@ async function ensureDir(filePath) {
|
|
|
1431
1799
|
}
|
|
1432
1800
|
|
|
1433
1801
|
// src/core/formats/json.ts
|
|
1434
|
-
import { readFile as readFile6, writeFile as
|
|
1802
|
+
import { readFile as readFile6, writeFile as writeFile3 } from "fs/promises";
|
|
1435
1803
|
import { existsSync as existsSync8 } from "fs";
|
|
1436
1804
|
import * as jsonc from "jsonc-parser";
|
|
1437
1805
|
async function readJsonConfig(filePath) {
|
|
@@ -1485,7 +1853,7 @@ async function writeJsonConfig(filePath, configKey, serverName, serverConfig) {
|
|
|
1485
1853
|
if (!content.endsWith("\n")) {
|
|
1486
1854
|
content += "\n";
|
|
1487
1855
|
}
|
|
1488
|
-
await
|
|
1856
|
+
await writeFile3(filePath, content, "utf-8");
|
|
1489
1857
|
}
|
|
1490
1858
|
async function removeJsonConfig(filePath, configKey, serverName) {
|
|
1491
1859
|
if (!existsSync8(filePath)) return false;
|
|
@@ -1505,12 +1873,12 @@ async function removeJsonConfig(filePath, configKey, serverName) {
|
|
|
1505
1873
|
if (!content.endsWith("\n")) {
|
|
1506
1874
|
content += "\n";
|
|
1507
1875
|
}
|
|
1508
|
-
await
|
|
1876
|
+
await writeFile3(filePath, content, "utf-8");
|
|
1509
1877
|
return true;
|
|
1510
1878
|
}
|
|
1511
1879
|
|
|
1512
1880
|
// src/core/formats/yaml.ts
|
|
1513
|
-
import { readFile as readFile7, writeFile as
|
|
1881
|
+
import { readFile as readFile7, writeFile as writeFile4 } from "fs/promises";
|
|
1514
1882
|
import { existsSync as existsSync9 } from "fs";
|
|
1515
1883
|
import yaml from "js-yaml";
|
|
1516
1884
|
async function readYamlConfig(filePath) {
|
|
@@ -1535,7 +1903,7 @@ async function writeYamlConfig(filePath, configKey, serverName, serverConfig) {
|
|
|
1535
1903
|
noRefs: true,
|
|
1536
1904
|
sortKeys: false
|
|
1537
1905
|
});
|
|
1538
|
-
await
|
|
1906
|
+
await writeFile4(filePath, content, "utf-8");
|
|
1539
1907
|
}
|
|
1540
1908
|
async function removeYamlConfig(filePath, configKey, serverName) {
|
|
1541
1909
|
if (!existsSync9(filePath)) return false;
|
|
@@ -1555,12 +1923,12 @@ async function removeYamlConfig(filePath, configKey, serverName) {
|
|
|
1555
1923
|
noRefs: true,
|
|
1556
1924
|
sortKeys: false
|
|
1557
1925
|
});
|
|
1558
|
-
await
|
|
1926
|
+
await writeFile4(filePath, content, "utf-8");
|
|
1559
1927
|
return true;
|
|
1560
1928
|
}
|
|
1561
1929
|
|
|
1562
1930
|
// src/core/formats/toml.ts
|
|
1563
|
-
import { readFile as readFile8, writeFile as
|
|
1931
|
+
import { readFile as readFile8, writeFile as writeFile5 } from "fs/promises";
|
|
1564
1932
|
import { existsSync as existsSync10 } from "fs";
|
|
1565
1933
|
import TOML from "@iarna/toml";
|
|
1566
1934
|
async function readTomlConfig(filePath) {
|
|
@@ -1580,7 +1948,7 @@ async function writeTomlConfig(filePath, configKey, serverName, serverConfig) {
|
|
|
1580
1948
|
}
|
|
1581
1949
|
const merged = deepMerge(existing, newEntry);
|
|
1582
1950
|
const content = TOML.stringify(merged);
|
|
1583
|
-
await
|
|
1951
|
+
await writeFile5(filePath, content, "utf-8");
|
|
1584
1952
|
}
|
|
1585
1953
|
async function removeTomlConfig(filePath, configKey, serverName) {
|
|
1586
1954
|
if (!existsSync10(filePath)) return false;
|
|
@@ -1595,12 +1963,13 @@ async function removeTomlConfig(filePath, configKey, serverName) {
|
|
|
1595
1963
|
if (!(serverName in current)) return false;
|
|
1596
1964
|
delete current[serverName];
|
|
1597
1965
|
const content = TOML.stringify(existing);
|
|
1598
|
-
await
|
|
1966
|
+
await writeFile5(filePath, content, "utf-8");
|
|
1599
1967
|
return true;
|
|
1600
1968
|
}
|
|
1601
1969
|
|
|
1602
1970
|
// src/core/formats/index.ts
|
|
1603
1971
|
async function readConfig(filePath, format) {
|
|
1972
|
+
debug(`reading config: ${filePath} (format: ${format})`);
|
|
1604
1973
|
switch (format) {
|
|
1605
1974
|
case "json":
|
|
1606
1975
|
case "jsonc":
|
|
@@ -1614,6 +1983,7 @@ async function readConfig(filePath, format) {
|
|
|
1614
1983
|
}
|
|
1615
1984
|
}
|
|
1616
1985
|
async function writeConfig(filePath, format, key, serverName, serverConfig) {
|
|
1986
|
+
debug(`writing config: ${filePath} (format: ${format}, key: ${key}, server: ${serverName})`);
|
|
1617
1987
|
switch (format) {
|
|
1618
1988
|
case "json":
|
|
1619
1989
|
case "jsonc":
|
|
@@ -1737,17 +2107,18 @@ function getTransform(providerId) {
|
|
|
1737
2107
|
}
|
|
1738
2108
|
|
|
1739
2109
|
// src/core/mcp/reader.ts
|
|
1740
|
-
import { join as
|
|
2110
|
+
import { join as join6 } from "path";
|
|
1741
2111
|
import { existsSync as existsSync11 } from "fs";
|
|
1742
2112
|
function resolveConfigPath(provider, scope, projectDir) {
|
|
1743
2113
|
if (scope === "project") {
|
|
1744
2114
|
if (!provider.configPathProject) return null;
|
|
1745
|
-
return
|
|
2115
|
+
return join6(projectDir ?? process.cwd(), provider.configPathProject);
|
|
1746
2116
|
}
|
|
1747
2117
|
return provider.configPathGlobal;
|
|
1748
2118
|
}
|
|
1749
2119
|
async function listMcpServers(provider, scope, projectDir) {
|
|
1750
2120
|
const configPath = resolveConfigPath(provider, scope, projectDir);
|
|
2121
|
+
debug(`listing MCP servers for ${provider.id} (${scope}) at ${configPath ?? "(none)"}`);
|
|
1751
2122
|
if (!configPath || !existsSync11(configPath)) return [];
|
|
1752
2123
|
try {
|
|
1753
2124
|
const config = await readConfig(configPath, provider.configFormat);
|
|
@@ -1797,6 +2168,8 @@ function buildConfig(provider, serverName, config) {
|
|
|
1797
2168
|
}
|
|
1798
2169
|
async function installMcpServer(provider, serverName, config, scope = "project", projectDir) {
|
|
1799
2170
|
const configPath = resolveConfigPath(provider, scope, projectDir);
|
|
2171
|
+
debug(`installing MCP server "${serverName}" for ${provider.id} (${scope})`);
|
|
2172
|
+
debug(` config path: ${configPath ?? "(none)"}`);
|
|
1800
2173
|
if (!configPath) {
|
|
1801
2174
|
return {
|
|
1802
2175
|
provider,
|
|
@@ -1808,6 +2181,8 @@ async function installMcpServer(provider, serverName, config, scope = "project",
|
|
|
1808
2181
|
}
|
|
1809
2182
|
try {
|
|
1810
2183
|
const transformedConfig = buildConfig(provider, serverName, config);
|
|
2184
|
+
const transform = getTransform(provider.id);
|
|
2185
|
+
debug(` transform applied: ${transform ? "yes" : "no"}`);
|
|
1811
2186
|
await writeConfig(
|
|
1812
2187
|
configPath,
|
|
1813
2188
|
provider.configFormat,
|
|
@@ -1860,11 +2235,50 @@ function buildServerConfig(source, transport, headers) {
|
|
|
1860
2235
|
};
|
|
1861
2236
|
}
|
|
1862
2237
|
|
|
2238
|
+
// src/core/mcp/lock.ts
|
|
2239
|
+
async function recordMcpInstall(serverName, source, sourceType, agents, isGlobal) {
|
|
2240
|
+
const lock = await readLockFile();
|
|
2241
|
+
const now = (/* @__PURE__ */ new Date()).toISOString();
|
|
2242
|
+
const existing = lock.mcpServers[serverName];
|
|
2243
|
+
lock.mcpServers[serverName] = {
|
|
2244
|
+
name: serverName,
|
|
2245
|
+
scopedName: serverName,
|
|
2246
|
+
source,
|
|
2247
|
+
sourceType,
|
|
2248
|
+
installedAt: existing?.installedAt ?? now,
|
|
2249
|
+
updatedAt: now,
|
|
2250
|
+
agents: [.../* @__PURE__ */ new Set([...existing?.agents ?? [], ...agents])],
|
|
2251
|
+
canonicalPath: "",
|
|
2252
|
+
isGlobal
|
|
2253
|
+
};
|
|
2254
|
+
await writeLockFile(lock);
|
|
2255
|
+
}
|
|
2256
|
+
async function removeMcpFromLock(serverName) {
|
|
2257
|
+
const lock = await readLockFile();
|
|
2258
|
+
if (!(serverName in lock.mcpServers)) return false;
|
|
2259
|
+
delete lock.mcpServers[serverName];
|
|
2260
|
+
await writeLockFile(lock);
|
|
2261
|
+
return true;
|
|
2262
|
+
}
|
|
2263
|
+
async function getTrackedMcpServers() {
|
|
2264
|
+
const lock = await readLockFile();
|
|
2265
|
+
return lock.mcpServers;
|
|
2266
|
+
}
|
|
2267
|
+
async function saveLastSelectedAgents(agents) {
|
|
2268
|
+
const lock = await readLockFile();
|
|
2269
|
+
lock.lastSelectedAgents = agents;
|
|
2270
|
+
await writeLockFile(lock);
|
|
2271
|
+
}
|
|
2272
|
+
async function getLastSelectedAgents() {
|
|
2273
|
+
const lock = await readLockFile();
|
|
2274
|
+
return lock.lastSelectedAgents;
|
|
2275
|
+
}
|
|
2276
|
+
|
|
1863
2277
|
// src/core/instructions/injector.ts
|
|
1864
|
-
import { readFile as readFile9, writeFile as
|
|
2278
|
+
import { readFile as readFile9, writeFile as writeFile6 } from "fs/promises";
|
|
1865
2279
|
import { existsSync as existsSync12 } from "fs";
|
|
1866
|
-
import { join as
|
|
1867
|
-
import { mkdir as
|
|
2280
|
+
import { join as join7, dirname as dirname3 } from "path";
|
|
2281
|
+
import { mkdir as mkdir3 } from "fs/promises";
|
|
1868
2282
|
var MARKER_START = "<!-- CAAMP:START -->";
|
|
1869
2283
|
var MARKER_END = "<!-- CAAMP:END -->";
|
|
1870
2284
|
var MARKER_PATTERN = /<!-- CAAMP:START -->[\s\S]*?<!-- CAAMP:END -->/;
|
|
@@ -1893,19 +2307,19 @@ ${MARKER_END}`;
|
|
|
1893
2307
|
}
|
|
1894
2308
|
async function inject(filePath, content) {
|
|
1895
2309
|
const block = buildBlock(content);
|
|
1896
|
-
await
|
|
2310
|
+
await mkdir3(dirname3(filePath), { recursive: true });
|
|
1897
2311
|
if (!existsSync12(filePath)) {
|
|
1898
|
-
await
|
|
2312
|
+
await writeFile6(filePath, block + "\n", "utf-8");
|
|
1899
2313
|
return "created";
|
|
1900
2314
|
}
|
|
1901
2315
|
const existing = await readFile9(filePath, "utf-8");
|
|
1902
2316
|
if (MARKER_PATTERN.test(existing)) {
|
|
1903
2317
|
const updated2 = existing.replace(MARKER_PATTERN, block);
|
|
1904
|
-
await
|
|
2318
|
+
await writeFile6(filePath, updated2, "utf-8");
|
|
1905
2319
|
return "updated";
|
|
1906
2320
|
}
|
|
1907
2321
|
const updated = block + "\n\n" + existing;
|
|
1908
|
-
await
|
|
2322
|
+
await writeFile6(filePath, updated, "utf-8");
|
|
1909
2323
|
return "added";
|
|
1910
2324
|
}
|
|
1911
2325
|
async function removeInjection(filePath) {
|
|
@@ -1914,10 +2328,10 @@ async function removeInjection(filePath) {
|
|
|
1914
2328
|
if (!MARKER_PATTERN.test(content)) return false;
|
|
1915
2329
|
const cleaned = content.replace(MARKER_PATTERN, "").replace(/^\n{2,}/, "\n").trim();
|
|
1916
2330
|
if (!cleaned) {
|
|
1917
|
-
const { rm:
|
|
1918
|
-
await
|
|
2331
|
+
const { rm: rm3 } = await import("fs/promises");
|
|
2332
|
+
await rm3(filePath);
|
|
1919
2333
|
} else {
|
|
1920
|
-
await
|
|
2334
|
+
await writeFile6(filePath, cleaned + "\n", "utf-8");
|
|
1921
2335
|
}
|
|
1922
2336
|
return true;
|
|
1923
2337
|
}
|
|
@@ -1925,7 +2339,7 @@ async function checkAllInjections(providers, projectDir, scope, expectedContent)
|
|
|
1925
2339
|
const results = [];
|
|
1926
2340
|
const checked = /* @__PURE__ */ new Set();
|
|
1927
2341
|
for (const provider of providers) {
|
|
1928
|
-
const filePath = scope === "global" ?
|
|
2342
|
+
const filePath = scope === "global" ? join7(provider.pathGlobal, provider.instructFile) : join7(projectDir, provider.instructFile);
|
|
1929
2343
|
if (checked.has(filePath)) continue;
|
|
1930
2344
|
checked.add(filePath);
|
|
1931
2345
|
const status = await checkInjection(filePath, expectedContent);
|
|
@@ -1942,7 +2356,7 @@ async function injectAll(providers, projectDir, scope, content) {
|
|
|
1942
2356
|
const results = /* @__PURE__ */ new Map();
|
|
1943
2357
|
const injected = /* @__PURE__ */ new Set();
|
|
1944
2358
|
for (const provider of providers) {
|
|
1945
|
-
const filePath = scope === "global" ?
|
|
2359
|
+
const filePath = scope === "global" ? join7(provider.pathGlobal, provider.instructFile) : join7(projectDir, provider.instructFile);
|
|
1946
2360
|
if (injected.has(filePath)) continue;
|
|
1947
2361
|
injected.add(filePath);
|
|
1948
2362
|
const action = await inject(filePath, content);
|
|
@@ -1979,6 +2393,398 @@ function groupByInstructFile(providers) {
|
|
|
1979
2393
|
return groups;
|
|
1980
2394
|
}
|
|
1981
2395
|
|
|
2396
|
+
// src/core/advanced/orchestration.ts
|
|
2397
|
+
import { existsSync as existsSync13, lstatSync as lstatSync2 } from "fs";
|
|
2398
|
+
import {
|
|
2399
|
+
cp as cp2,
|
|
2400
|
+
mkdir as mkdir4,
|
|
2401
|
+
readFile as readFile10,
|
|
2402
|
+
readlink as readlink2,
|
|
2403
|
+
rm as rm2,
|
|
2404
|
+
symlink as symlink2,
|
|
2405
|
+
writeFile as writeFile7
|
|
2406
|
+
} from "fs/promises";
|
|
2407
|
+
import { homedir as homedir4, tmpdir } from "os";
|
|
2408
|
+
import { basename as basename2, dirname as dirname4, join as join8 } from "path";
|
|
2409
|
+
var PRIORITY_ORDER = {
|
|
2410
|
+
high: 0,
|
|
2411
|
+
medium: 1,
|
|
2412
|
+
low: 2
|
|
2413
|
+
};
|
|
2414
|
+
var CANONICAL_SKILLS_DIR = join8(homedir4(), ".agents", "skills");
|
|
2415
|
+
function selectProvidersByMinimumPriority(providers, minimumPriority = "low") {
|
|
2416
|
+
const maxRank = PRIORITY_ORDER[minimumPriority];
|
|
2417
|
+
return [...providers].filter((provider) => PRIORITY_ORDER[provider.priority] <= maxRank).sort((a, b) => PRIORITY_ORDER[a.priority] - PRIORITY_ORDER[b.priority]);
|
|
2418
|
+
}
|
|
2419
|
+
function resolveSkillLinkPath(provider, skillName, isGlobal, projectDir) {
|
|
2420
|
+
const skillDir = isGlobal ? provider.pathSkills : join8(projectDir, provider.pathProjectSkills);
|
|
2421
|
+
return join8(skillDir, skillName);
|
|
2422
|
+
}
|
|
2423
|
+
async function snapshotConfigs(paths) {
|
|
2424
|
+
const snapshots = /* @__PURE__ */ new Map();
|
|
2425
|
+
for (const path of paths) {
|
|
2426
|
+
if (!path || snapshots.has(path)) continue;
|
|
2427
|
+
if (!existsSync13(path)) {
|
|
2428
|
+
snapshots.set(path, null);
|
|
2429
|
+
continue;
|
|
2430
|
+
}
|
|
2431
|
+
snapshots.set(path, await readFile10(path, "utf-8"));
|
|
2432
|
+
}
|
|
2433
|
+
return snapshots;
|
|
2434
|
+
}
|
|
2435
|
+
async function restoreConfigSnapshots(snapshots) {
|
|
2436
|
+
for (const [path, content] of snapshots) {
|
|
2437
|
+
if (content === null) {
|
|
2438
|
+
await rm2(path, { force: true });
|
|
2439
|
+
continue;
|
|
2440
|
+
}
|
|
2441
|
+
await mkdir4(dirname4(path), { recursive: true });
|
|
2442
|
+
await writeFile7(path, content, "utf-8");
|
|
2443
|
+
}
|
|
2444
|
+
}
|
|
2445
|
+
async function snapshotSkillState(providerTargets, operation, projectDir, backupRoot) {
|
|
2446
|
+
const skillName = operation.skillName;
|
|
2447
|
+
const isGlobal = operation.isGlobal ?? true;
|
|
2448
|
+
const canonicalPath = join8(CANONICAL_SKILLS_DIR, skillName);
|
|
2449
|
+
const canonicalExisted = existsSync13(canonicalPath);
|
|
2450
|
+
const canonicalBackupPath = join8(backupRoot, "canonical", skillName);
|
|
2451
|
+
if (canonicalExisted) {
|
|
2452
|
+
await mkdir4(dirname4(canonicalBackupPath), { recursive: true });
|
|
2453
|
+
await cp2(canonicalPath, canonicalBackupPath, { recursive: true });
|
|
2454
|
+
}
|
|
2455
|
+
const pathSnapshots = [];
|
|
2456
|
+
for (const provider of providerTargets) {
|
|
2457
|
+
const linkPath = resolveSkillLinkPath(provider, skillName, isGlobal, projectDir);
|
|
2458
|
+
if (!existsSync13(linkPath)) {
|
|
2459
|
+
pathSnapshots.push({ linkPath, state: "missing" });
|
|
2460
|
+
continue;
|
|
2461
|
+
}
|
|
2462
|
+
const stat = lstatSync2(linkPath);
|
|
2463
|
+
if (stat.isSymbolicLink()) {
|
|
2464
|
+
pathSnapshots.push({
|
|
2465
|
+
linkPath,
|
|
2466
|
+
state: "symlink",
|
|
2467
|
+
symlinkTarget: await readlink2(linkPath)
|
|
2468
|
+
});
|
|
2469
|
+
continue;
|
|
2470
|
+
}
|
|
2471
|
+
const backupPath = join8(backupRoot, "links", provider.id, `${skillName}-${basename2(linkPath)}`);
|
|
2472
|
+
await mkdir4(dirname4(backupPath), { recursive: true });
|
|
2473
|
+
if (stat.isDirectory()) {
|
|
2474
|
+
await cp2(linkPath, backupPath, { recursive: true });
|
|
2475
|
+
pathSnapshots.push({ linkPath, state: "directory", backupPath });
|
|
2476
|
+
continue;
|
|
2477
|
+
}
|
|
2478
|
+
await cp2(linkPath, backupPath);
|
|
2479
|
+
pathSnapshots.push({ linkPath, state: "file", backupPath });
|
|
2480
|
+
}
|
|
2481
|
+
return {
|
|
2482
|
+
skillName,
|
|
2483
|
+
isGlobal,
|
|
2484
|
+
canonicalPath,
|
|
2485
|
+
canonicalBackupPath: canonicalExisted ? canonicalBackupPath : void 0,
|
|
2486
|
+
canonicalExisted,
|
|
2487
|
+
pathSnapshots
|
|
2488
|
+
};
|
|
2489
|
+
}
|
|
2490
|
+
async function restoreSkillSnapshot(snapshot) {
|
|
2491
|
+
if (existsSync13(snapshot.canonicalPath)) {
|
|
2492
|
+
await rm2(snapshot.canonicalPath, { recursive: true, force: true });
|
|
2493
|
+
}
|
|
2494
|
+
if (snapshot.canonicalExisted && snapshot.canonicalBackupPath && existsSync13(snapshot.canonicalBackupPath)) {
|
|
2495
|
+
await mkdir4(dirname4(snapshot.canonicalPath), { recursive: true });
|
|
2496
|
+
await cp2(snapshot.canonicalBackupPath, snapshot.canonicalPath, { recursive: true });
|
|
2497
|
+
}
|
|
2498
|
+
for (const pathSnapshot of snapshot.pathSnapshots) {
|
|
2499
|
+
await rm2(pathSnapshot.linkPath, { recursive: true, force: true });
|
|
2500
|
+
if (pathSnapshot.state === "missing") continue;
|
|
2501
|
+
await mkdir4(dirname4(pathSnapshot.linkPath), { recursive: true });
|
|
2502
|
+
if (pathSnapshot.state === "symlink" && pathSnapshot.symlinkTarget) {
|
|
2503
|
+
const linkType = process.platform === "win32" ? "junction" : "dir";
|
|
2504
|
+
await symlink2(pathSnapshot.symlinkTarget, pathSnapshot.linkPath, linkType);
|
|
2505
|
+
continue;
|
|
2506
|
+
}
|
|
2507
|
+
if ((pathSnapshot.state === "directory" || pathSnapshot.state === "file") && pathSnapshot.backupPath) {
|
|
2508
|
+
if (pathSnapshot.state === "directory") {
|
|
2509
|
+
await cp2(pathSnapshot.backupPath, pathSnapshot.linkPath, { recursive: true });
|
|
2510
|
+
} else {
|
|
2511
|
+
await cp2(pathSnapshot.backupPath, pathSnapshot.linkPath);
|
|
2512
|
+
}
|
|
2513
|
+
}
|
|
2514
|
+
}
|
|
2515
|
+
}
|
|
2516
|
+
async function installBatchWithRollback(options) {
|
|
2517
|
+
const projectDir = options.projectDir ?? process.cwd();
|
|
2518
|
+
const minimumPriority = options.minimumPriority ?? "low";
|
|
2519
|
+
const mcpOps = options.mcp ?? [];
|
|
2520
|
+
const skillOps = options.skills ?? [];
|
|
2521
|
+
const baseProviders = options.providers ?? getInstalledProviders();
|
|
2522
|
+
const providers = selectProvidersByMinimumPriority(baseProviders, minimumPriority);
|
|
2523
|
+
const configPaths = providers.flatMap((provider) => {
|
|
2524
|
+
const paths = [];
|
|
2525
|
+
for (const operation of mcpOps) {
|
|
2526
|
+
const path = resolveConfigPath(provider, operation.scope ?? "project", projectDir);
|
|
2527
|
+
if (path) paths.push(path);
|
|
2528
|
+
}
|
|
2529
|
+
return paths;
|
|
2530
|
+
});
|
|
2531
|
+
const configSnapshots = await snapshotConfigs(configPaths);
|
|
2532
|
+
const backupRoot = join8(
|
|
2533
|
+
tmpdir(),
|
|
2534
|
+
`caamp-skill-backup-${Date.now()}-${Math.random().toString(36).slice(2, 8)}`
|
|
2535
|
+
);
|
|
2536
|
+
const skillSnapshots = await Promise.all(
|
|
2537
|
+
skillOps.map((operation) => snapshotSkillState(providers, operation, projectDir, backupRoot))
|
|
2538
|
+
);
|
|
2539
|
+
const appliedSkills = [];
|
|
2540
|
+
const rollbackErrors = [];
|
|
2541
|
+
let mcpApplied = 0;
|
|
2542
|
+
let skillsApplied = 0;
|
|
2543
|
+
let rollbackPerformed = false;
|
|
2544
|
+
try {
|
|
2545
|
+
for (const operation of mcpOps) {
|
|
2546
|
+
const scope = operation.scope ?? "project";
|
|
2547
|
+
for (const provider of providers) {
|
|
2548
|
+
const result = await installMcpServer(
|
|
2549
|
+
provider,
|
|
2550
|
+
operation.serverName,
|
|
2551
|
+
operation.config,
|
|
2552
|
+
scope,
|
|
2553
|
+
projectDir
|
|
2554
|
+
);
|
|
2555
|
+
if (!result.success) {
|
|
2556
|
+
throw new Error(result.error ?? `Failed MCP install for ${provider.id}`);
|
|
2557
|
+
}
|
|
2558
|
+
mcpApplied += 1;
|
|
2559
|
+
}
|
|
2560
|
+
}
|
|
2561
|
+
for (const operation of skillOps) {
|
|
2562
|
+
const isGlobal = operation.isGlobal ?? true;
|
|
2563
|
+
const result = await installSkill(
|
|
2564
|
+
operation.sourcePath,
|
|
2565
|
+
operation.skillName,
|
|
2566
|
+
providers,
|
|
2567
|
+
isGlobal,
|
|
2568
|
+
projectDir
|
|
2569
|
+
);
|
|
2570
|
+
const linkedProviders = providers.filter((provider) => result.linkedAgents.includes(provider.id));
|
|
2571
|
+
appliedSkills.push({
|
|
2572
|
+
skillName: operation.skillName,
|
|
2573
|
+
isGlobal,
|
|
2574
|
+
linkedProviders
|
|
2575
|
+
});
|
|
2576
|
+
if (result.errors.length > 0) {
|
|
2577
|
+
throw new Error(result.errors.join("; "));
|
|
2578
|
+
}
|
|
2579
|
+
skillsApplied += 1;
|
|
2580
|
+
}
|
|
2581
|
+
await rm2(backupRoot, { recursive: true, force: true });
|
|
2582
|
+
return {
|
|
2583
|
+
success: true,
|
|
2584
|
+
providerIds: providers.map((provider) => provider.id),
|
|
2585
|
+
mcpApplied,
|
|
2586
|
+
skillsApplied,
|
|
2587
|
+
rollbackPerformed: false,
|
|
2588
|
+
rollbackErrors: []
|
|
2589
|
+
};
|
|
2590
|
+
} catch (error) {
|
|
2591
|
+
rollbackPerformed = true;
|
|
2592
|
+
for (const applied of [...appliedSkills].reverse()) {
|
|
2593
|
+
try {
|
|
2594
|
+
await removeSkill(applied.skillName, applied.linkedProviders, applied.isGlobal, projectDir);
|
|
2595
|
+
} catch (err) {
|
|
2596
|
+
rollbackErrors.push(err instanceof Error ? err.message : String(err));
|
|
2597
|
+
}
|
|
2598
|
+
}
|
|
2599
|
+
try {
|
|
2600
|
+
await restoreConfigSnapshots(configSnapshots);
|
|
2601
|
+
} catch (err) {
|
|
2602
|
+
rollbackErrors.push(err instanceof Error ? err.message : String(err));
|
|
2603
|
+
}
|
|
2604
|
+
for (const snapshot of skillSnapshots) {
|
|
2605
|
+
try {
|
|
2606
|
+
await restoreSkillSnapshot(snapshot);
|
|
2607
|
+
} catch (err) {
|
|
2608
|
+
rollbackErrors.push(err instanceof Error ? err.message : String(err));
|
|
2609
|
+
}
|
|
2610
|
+
}
|
|
2611
|
+
await rm2(backupRoot, { recursive: true, force: true });
|
|
2612
|
+
return {
|
|
2613
|
+
success: false,
|
|
2614
|
+
providerIds: providers.map((provider) => provider.id),
|
|
2615
|
+
mcpApplied,
|
|
2616
|
+
skillsApplied,
|
|
2617
|
+
rollbackPerformed,
|
|
2618
|
+
rollbackErrors,
|
|
2619
|
+
error: error instanceof Error ? error.message : String(error)
|
|
2620
|
+
};
|
|
2621
|
+
}
|
|
2622
|
+
}
|
|
2623
|
+
function stableStringify(value) {
|
|
2624
|
+
if (Array.isArray(value)) {
|
|
2625
|
+
return `[${value.map(stableStringify).join(",")}]`;
|
|
2626
|
+
}
|
|
2627
|
+
if (value && typeof value === "object") {
|
|
2628
|
+
const record = value;
|
|
2629
|
+
const keys = Object.keys(record).sort();
|
|
2630
|
+
return `{${keys.map((key) => `${JSON.stringify(key)}:${stableStringify(record[key])}`).join(",")}}`;
|
|
2631
|
+
}
|
|
2632
|
+
return JSON.stringify(value);
|
|
2633
|
+
}
|
|
2634
|
+
async function detectMcpConfigConflicts(providers, operations, projectDir = process.cwd()) {
|
|
2635
|
+
const conflicts = [];
|
|
2636
|
+
for (const provider of providers) {
|
|
2637
|
+
for (const operation of operations) {
|
|
2638
|
+
const scope = operation.scope ?? "project";
|
|
2639
|
+
if (operation.config.type && !provider.supportedTransports.includes(operation.config.type)) {
|
|
2640
|
+
conflicts.push({
|
|
2641
|
+
providerId: provider.id,
|
|
2642
|
+
serverName: operation.serverName,
|
|
2643
|
+
scope,
|
|
2644
|
+
code: "unsupported-transport",
|
|
2645
|
+
message: `${provider.id} does not support transport ${operation.config.type}`
|
|
2646
|
+
});
|
|
2647
|
+
}
|
|
2648
|
+
if (operation.config.headers && !provider.supportsHeaders) {
|
|
2649
|
+
conflicts.push({
|
|
2650
|
+
providerId: provider.id,
|
|
2651
|
+
serverName: operation.serverName,
|
|
2652
|
+
scope,
|
|
2653
|
+
code: "unsupported-headers",
|
|
2654
|
+
message: `${provider.id} does not support header configuration`
|
|
2655
|
+
});
|
|
2656
|
+
}
|
|
2657
|
+
const existingEntries = await listMcpServers(provider, scope, projectDir);
|
|
2658
|
+
const current = existingEntries.find((entry) => entry.name === operation.serverName);
|
|
2659
|
+
if (!current) continue;
|
|
2660
|
+
const transform = getTransform(provider.id);
|
|
2661
|
+
const desired = transform ? transform(operation.serverName, operation.config) : operation.config;
|
|
2662
|
+
if (stableStringify(current.config) !== stableStringify(desired)) {
|
|
2663
|
+
conflicts.push({
|
|
2664
|
+
providerId: provider.id,
|
|
2665
|
+
serverName: operation.serverName,
|
|
2666
|
+
scope,
|
|
2667
|
+
code: "existing-mismatch",
|
|
2668
|
+
message: `${provider.id} has existing config mismatch for ${operation.serverName}`
|
|
2669
|
+
});
|
|
2670
|
+
}
|
|
2671
|
+
}
|
|
2672
|
+
}
|
|
2673
|
+
return conflicts;
|
|
2674
|
+
}
|
|
2675
|
+
async function applyMcpInstallWithPolicy(providers, operations, policy = "fail", projectDir = process.cwd()) {
|
|
2676
|
+
const conflicts = await detectMcpConfigConflicts(providers, operations, projectDir);
|
|
2677
|
+
const conflictKey = (providerId, serverName, scope) => `${providerId}::${serverName}::${scope}`;
|
|
2678
|
+
const conflictMap = /* @__PURE__ */ new Map();
|
|
2679
|
+
for (const conflict of conflicts) {
|
|
2680
|
+
conflictMap.set(conflictKey(conflict.providerId, conflict.serverName, conflict.scope), conflict);
|
|
2681
|
+
}
|
|
2682
|
+
if (policy === "fail" && conflicts.length > 0) {
|
|
2683
|
+
return { conflicts, applied: [], skipped: [] };
|
|
2684
|
+
}
|
|
2685
|
+
const applied = [];
|
|
2686
|
+
const skipped = [];
|
|
2687
|
+
for (const provider of providers) {
|
|
2688
|
+
for (const operation of operations) {
|
|
2689
|
+
const scope = operation.scope ?? "project";
|
|
2690
|
+
const key = conflictKey(provider.id, operation.serverName, scope);
|
|
2691
|
+
const conflict = conflictMap.get(key);
|
|
2692
|
+
if (policy === "skip" && conflict) {
|
|
2693
|
+
skipped.push({
|
|
2694
|
+
providerId: provider.id,
|
|
2695
|
+
serverName: operation.serverName,
|
|
2696
|
+
scope,
|
|
2697
|
+
reason: conflict.code
|
|
2698
|
+
});
|
|
2699
|
+
continue;
|
|
2700
|
+
}
|
|
2701
|
+
const result = await installMcpServer(
|
|
2702
|
+
provider,
|
|
2703
|
+
operation.serverName,
|
|
2704
|
+
operation.config,
|
|
2705
|
+
scope,
|
|
2706
|
+
projectDir
|
|
2707
|
+
);
|
|
2708
|
+
applied.push(result);
|
|
2709
|
+
}
|
|
2710
|
+
}
|
|
2711
|
+
return { conflicts, applied, skipped };
|
|
2712
|
+
}
|
|
2713
|
+
async function updateInstructionsSingleOperation(providers, content, scope = "project", projectDir = process.cwd()) {
|
|
2714
|
+
const actions = await injectAll(providers, projectDir, scope, content);
|
|
2715
|
+
const groupedByFile = groupByInstructFile(providers);
|
|
2716
|
+
const summary = {
|
|
2717
|
+
scope,
|
|
2718
|
+
updatedFiles: actions.size,
|
|
2719
|
+
actions: []
|
|
2720
|
+
};
|
|
2721
|
+
for (const [filePath, action] of actions.entries()) {
|
|
2722
|
+
const providersForFile = providers.filter((provider) => {
|
|
2723
|
+
const expectedPath = scope === "global" ? join8(provider.pathGlobal, provider.instructFile) : join8(projectDir, provider.instructFile);
|
|
2724
|
+
return expectedPath === filePath;
|
|
2725
|
+
});
|
|
2726
|
+
const fallback = groupedByFile.get(basename2(filePath)) ?? [];
|
|
2727
|
+
const selected = providersForFile.length > 0 ? providersForFile : fallback;
|
|
2728
|
+
summary.actions.push({
|
|
2729
|
+
file: filePath,
|
|
2730
|
+
action,
|
|
2731
|
+
providers: selected.map((provider) => provider.id),
|
|
2732
|
+
configFormats: Array.from(new Set(selected.map((provider) => provider.configFormat)))
|
|
2733
|
+
});
|
|
2734
|
+
}
|
|
2735
|
+
return summary;
|
|
2736
|
+
}
|
|
2737
|
+
async function configureProviderGlobalAndProject(provider, options) {
|
|
2738
|
+
const projectDir = options.projectDir ?? process.cwd();
|
|
2739
|
+
const globalOps = options.globalMcp ?? [];
|
|
2740
|
+
const projectOps = options.projectMcp ?? [];
|
|
2741
|
+
const globalResults = [];
|
|
2742
|
+
for (const operation of globalOps) {
|
|
2743
|
+
globalResults.push(await installMcpServer(
|
|
2744
|
+
provider,
|
|
2745
|
+
operation.serverName,
|
|
2746
|
+
operation.config,
|
|
2747
|
+
"global",
|
|
2748
|
+
projectDir
|
|
2749
|
+
));
|
|
2750
|
+
}
|
|
2751
|
+
const projectResults = [];
|
|
2752
|
+
for (const operation of projectOps) {
|
|
2753
|
+
projectResults.push(await installMcpServer(
|
|
2754
|
+
provider,
|
|
2755
|
+
operation.serverName,
|
|
2756
|
+
operation.config,
|
|
2757
|
+
"project",
|
|
2758
|
+
projectDir
|
|
2759
|
+
));
|
|
2760
|
+
}
|
|
2761
|
+
const instructionResults = {};
|
|
2762
|
+
const instructionContent = options.instructionContent;
|
|
2763
|
+
if (typeof instructionContent === "string") {
|
|
2764
|
+
instructionResults.global = await injectAll([provider], projectDir, "global", instructionContent);
|
|
2765
|
+
instructionResults.project = await injectAll([provider], projectDir, "project", instructionContent);
|
|
2766
|
+
} else if (instructionContent) {
|
|
2767
|
+
if (instructionContent.global) {
|
|
2768
|
+
instructionResults.global = await injectAll([provider], projectDir, "global", instructionContent.global);
|
|
2769
|
+
}
|
|
2770
|
+
if (instructionContent.project) {
|
|
2771
|
+
instructionResults.project = await injectAll([provider], projectDir, "project", instructionContent.project);
|
|
2772
|
+
}
|
|
2773
|
+
}
|
|
2774
|
+
return {
|
|
2775
|
+
providerId: provider.id,
|
|
2776
|
+
configPaths: {
|
|
2777
|
+
global: resolveConfigPath(provider, "global", projectDir),
|
|
2778
|
+
project: resolveConfigPath(provider, "project", projectDir)
|
|
2779
|
+
},
|
|
2780
|
+
mcp: {
|
|
2781
|
+
global: globalResults,
|
|
2782
|
+
project: projectResults
|
|
2783
|
+
},
|
|
2784
|
+
instructions: instructionResults
|
|
2785
|
+
};
|
|
2786
|
+
}
|
|
2787
|
+
|
|
1982
2788
|
export {
|
|
1983
2789
|
getAllProviders,
|
|
1984
2790
|
getProvider,
|
|
@@ -1989,6 +2795,10 @@ export {
|
|
|
1989
2795
|
getInstructionFiles,
|
|
1990
2796
|
getProviderCount,
|
|
1991
2797
|
getRegistryVersion,
|
|
2798
|
+
setVerbose,
|
|
2799
|
+
setQuiet,
|
|
2800
|
+
isVerbose,
|
|
2801
|
+
isQuiet,
|
|
1992
2802
|
detectProvider,
|
|
1993
2803
|
detectAllProviders,
|
|
1994
2804
|
getInstalledProviders,
|
|
@@ -1999,20 +2809,25 @@ export {
|
|
|
1999
2809
|
removeSkill,
|
|
2000
2810
|
listCanonicalSkills,
|
|
2001
2811
|
readLockFile,
|
|
2002
|
-
recordMcpInstall,
|
|
2003
|
-
removeMcpFromLock,
|
|
2004
|
-
getTrackedMcpServers,
|
|
2005
|
-
saveLastSelectedAgents,
|
|
2006
|
-
getLastSelectedAgents,
|
|
2007
2812
|
recordSkillInstall,
|
|
2008
2813
|
removeSkillFromLock,
|
|
2009
2814
|
getTrackedSkills,
|
|
2010
2815
|
checkSkillUpdate,
|
|
2816
|
+
formatNetworkError,
|
|
2011
2817
|
MarketplaceClient,
|
|
2012
2818
|
parseSkillFile,
|
|
2013
2819
|
discoverSkill,
|
|
2014
2820
|
discoverSkills,
|
|
2015
2821
|
discoverSkillsMulti,
|
|
2822
|
+
RECOMMENDATION_ERROR_CODES,
|
|
2823
|
+
tokenizeCriteriaValue,
|
|
2824
|
+
validateRecommendationCriteria,
|
|
2825
|
+
normalizeRecommendationCriteria,
|
|
2826
|
+
scoreSkillRecommendation,
|
|
2827
|
+
rankSkills,
|
|
2828
|
+
formatSkillRecommendations,
|
|
2829
|
+
searchSkills,
|
|
2830
|
+
recommendSkills2 as recommendSkills,
|
|
2016
2831
|
scanFile,
|
|
2017
2832
|
scanDirectory,
|
|
2018
2833
|
toSarif,
|
|
@@ -2031,12 +2846,23 @@ export {
|
|
|
2031
2846
|
installMcpServer,
|
|
2032
2847
|
installMcpServerToAll,
|
|
2033
2848
|
buildServerConfig,
|
|
2849
|
+
recordMcpInstall,
|
|
2850
|
+
removeMcpFromLock,
|
|
2851
|
+
getTrackedMcpServers,
|
|
2852
|
+
saveLastSelectedAgents,
|
|
2853
|
+
getLastSelectedAgents,
|
|
2034
2854
|
checkInjection,
|
|
2035
2855
|
inject,
|
|
2036
2856
|
removeInjection,
|
|
2037
2857
|
checkAllInjections,
|
|
2038
2858
|
injectAll,
|
|
2039
2859
|
generateInjectionContent,
|
|
2040
|
-
groupByInstructFile
|
|
2860
|
+
groupByInstructFile,
|
|
2861
|
+
selectProvidersByMinimumPriority,
|
|
2862
|
+
installBatchWithRollback,
|
|
2863
|
+
detectMcpConfigConflicts,
|
|
2864
|
+
applyMcpInstallWithPolicy,
|
|
2865
|
+
updateInstructionsSingleOperation,
|
|
2866
|
+
configureProviderGlobalAndProject
|
|
2041
2867
|
};
|
|
2042
|
-
//# sourceMappingURL=chunk-
|
|
2868
|
+
//# sourceMappingURL=chunk-ZYINKJDE.js.map
|