@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.
@@ -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
- execFileSync("which", [binary], { stdio: "pipe" });
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/mcp/lock.ts
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 writeLockFile2(lock);
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 writeLockFile2(lock);
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
- try {
627
- const response = await fetch(`${API_BASE}?${params}`);
628
- if (!response.ok) return [];
629
- const data = await response.json();
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 params = new URLSearchParams({
637
- search: scopedName,
638
- limit: "1"
639
- });
640
- try {
641
- const response = await fetch(`${API_BASE}?${params}`);
642
- if (!response.ok) return null;
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
- return match ? toResult(match) : null;
648
- } catch {
649
- return null;
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
- try {
673
- const params = new URLSearchParams({
674
- q: query,
675
- limit: String(limit)
676
- });
677
- const response = await fetch(`${API_BASE2}/search?${params}`);
678
- if (!response.ok) return [];
679
- const data = await response.json();
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
- /** Search all marketplaces and deduplicate results */
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 promises = this.adapters.map(
703
- (adapter) => adapter.search(query, limit).catch(() => [])
704
- );
705
- const allResults = await Promise.all(promises);
706
- const flat = allResults.flat();
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
- /** Get a specific skill by scoped name */
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
- const result = await adapter.getSkill(scopedName).catch(() => null);
722
- if (result) return result;
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 join6 } from "path";
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 = join6(skillDir, "SKILL.md");
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 = join6(rootDir, entry.name);
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 writeFile4 } from "fs/promises";
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 writeFile4(filePath, content, "utf-8");
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 writeFile4(filePath, content, "utf-8");
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 writeFile5 } from "fs/promises";
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 writeFile5(filePath, content, "utf-8");
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 writeFile5(filePath, content, "utf-8");
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 writeFile6 } from "fs/promises";
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 writeFile6(filePath, content, "utf-8");
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 writeFile6(filePath, content, "utf-8");
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 join7 } from "path";
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 join7(projectDir ?? process.cwd(), provider.configPathProject);
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 writeFile7 } from "fs/promises";
2278
+ import { readFile as readFile9, writeFile as writeFile6 } from "fs/promises";
1865
2279
  import { existsSync as existsSync12 } from "fs";
1866
- import { join as join8, dirname as dirname4 } from "path";
1867
- import { mkdir as mkdir4 } from "fs/promises";
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 mkdir4(dirname4(filePath), { recursive: true });
2310
+ await mkdir3(dirname3(filePath), { recursive: true });
1897
2311
  if (!existsSync12(filePath)) {
1898
- await writeFile7(filePath, block + "\n", "utf-8");
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 writeFile7(filePath, updated2, "utf-8");
2318
+ await writeFile6(filePath, updated2, "utf-8");
1905
2319
  return "updated";
1906
2320
  }
1907
2321
  const updated = block + "\n\n" + existing;
1908
- await writeFile7(filePath, updated, "utf-8");
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: rm2 } = await import("fs/promises");
1918
- await rm2(filePath);
2331
+ const { rm: rm3 } = await import("fs/promises");
2332
+ await rm3(filePath);
1919
2333
  } else {
1920
- await writeFile7(filePath, cleaned + "\n", "utf-8");
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" ? join8(provider.pathGlobal, provider.instructFile) : join8(projectDir, provider.instructFile);
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" ? join8(provider.pathGlobal, provider.instructFile) : join8(projectDir, provider.instructFile);
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-RW745KDU.js.map
2868
+ //# sourceMappingURL=chunk-ZYINKJDE.js.map