@codemieai/cdk 0.1.543 → 0.1.544

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/dist/index.js CHANGED
@@ -1,6 +1,6 @@
1
1
  import ora from 'ora';
2
2
  import * as crypto from 'crypto';
3
- import { CodeMieClient, DataSourceType } from 'codemie-sdk';
3
+ import { CodeMieClient, DataSourceType, SkillVisibility } from 'codemie-sdk';
4
4
  import * as fs from 'fs';
5
5
  import * as path from 'path';
6
6
  import * as yaml6 from 'yaml';
@@ -689,11 +689,23 @@ var init_fileUtils = __esm({
689
689
  }
690
690
  });
691
691
 
692
+ // src/lib/skillUtils.ts
693
+ function isProjectOwnedSkill(skill, projectName) {
694
+ return skill.project.trim().toLowerCase() === projectName.trim().toLowerCase();
695
+ }
696
+ var init_skillUtils = __esm({
697
+ "src/lib/skillUtils.ts"() {
698
+ }
699
+ });
700
+
692
701
  // src/lib/backupTransformers.ts
693
702
  var backupTransformers_exports = {};
694
703
  __export(backupTransformers_exports, {
704
+ collectReferencedSkillIds: () => collectReferencedSkillIds,
705
+ partitionSkillsForBackupYaml: () => partitionSkillsForBackupYaml,
695
706
  prepareAssistantForYaml: () => prepareAssistantForYaml,
696
707
  prepareDatasourceForYaml: () => prepareDatasourceForYaml,
708
+ prepareImportedSkillForYaml: () => prepareImportedSkillForYaml,
697
709
  prepareSkillForYaml: () => prepareSkillForYaml,
698
710
  prepareWorkflowForYaml: () => prepareWorkflowForYaml,
699
711
  transformMcpServer: () => transformMcpServer,
@@ -850,6 +862,36 @@ function prepareWorkflowForYaml(workflow, state, assistants, backupDir) {
850
862
  };
851
863
  return resource;
852
864
  }
865
+ function collectReferencedSkillIds(assistants) {
866
+ const referencedSkillIds = /* @__PURE__ */ new Set();
867
+ for (const assistant of assistants) {
868
+ const assistantData = assistant;
869
+ for (const skillId of assistantData.skill_ids || []) {
870
+ referencedSkillIds.add(skillId);
871
+ }
872
+ }
873
+ return referencedSkillIds;
874
+ }
875
+ function partitionSkillsForBackupYaml(skills, projectName, referencedSkillIds) {
876
+ const ownedSkills = [];
877
+ const importedSkills = [];
878
+ for (const skill of skills) {
879
+ if (isProjectOwnedSkill(skill, projectName)) {
880
+ ownedSkills.push(skill);
881
+ continue;
882
+ }
883
+ if (referencedSkillIds.has(skill.id)) {
884
+ importedSkills.push(skill);
885
+ }
886
+ }
887
+ return { ownedSkills, importedSkills };
888
+ }
889
+ function prepareImportedSkillForYaml(skill) {
890
+ return {
891
+ name: skill.name,
892
+ id: skill.id
893
+ };
894
+ }
853
895
  function prepareSkillForYaml(skill, state) {
854
896
  const resource = skillResponseToResource(skill);
855
897
  if (!state.resources.skills) {
@@ -870,9 +912,17 @@ var init_backupTransformers = __esm({
870
912
  init_converters();
871
913
  init_fileUtils();
872
914
  init_logger();
915
+ init_skillUtils();
873
916
  init_typeGuards();
874
917
  }
875
918
  });
919
+
920
+ // src/lib/nameUtils.ts
921
+ function namesEqual(a, b) {
922
+ return a.toLowerCase() === b.toLowerCase();
923
+ }
924
+
925
+ // src/lib/codemieConfigLoader.ts
876
926
  var CodemieConfigLoader = class {
877
927
  appConfig;
878
928
  constructor(appConfig) {
@@ -967,6 +1017,14 @@ var CodemieConfigLoader = class {
967
1017
  }
968
1018
  }
969
1019
  }
1020
+ for (const skill of config.imported?.skills || []) {
1021
+ if (!skill.name) {
1022
+ errors.push("Imported skill entry is missing required field: name");
1023
+ }
1024
+ if (!skill.id) {
1025
+ errors.push(`Imported skill "${skill.name || "<unknown>"}" is missing required field: id`);
1026
+ }
1027
+ }
970
1028
  return {
971
1029
  valid: errors.length === 0,
972
1030
  errors
@@ -1181,6 +1239,7 @@ Import chain: ${[...visitedFiles].join(" \u2192 ")} \u2192 ${normalizedPath}`
1181
1239
  * - imported.integrations (array): searches by 'alias' field
1182
1240
  * - imported.assistants (array): searches by 'name' field
1183
1241
  * - imported.datasources (array): searches by 'name' field
1242
+ * - imported.skills (array): searches by 'name' field
1184
1243
  */
1185
1244
  resolveReference(config, ref, context) {
1186
1245
  const parts = ref.split(".");
@@ -1204,9 +1263,16 @@ Import chain: ${[...visitedFiles].join(" \u2192 ")} \u2192 ${normalizedPath}`
1204
1263
  if (Array.isArray(current) && hasMorePathSegments) {
1205
1264
  const nextPathSegment = parts[i + 1];
1206
1265
  const searchField = part === "integrations" ? "alias" : "name";
1207
- const foundItem = current.find(
1208
- (item) => typeof item === "object" && item !== null && item[searchField] === nextPathSegment
1209
- );
1266
+ const foundItem = current.find((item) => {
1267
+ if (typeof item !== "object" || item === null) {
1268
+ return false;
1269
+ }
1270
+ const fieldValue = item[searchField];
1271
+ if (typeof fieldValue !== "string") {
1272
+ return false;
1273
+ }
1274
+ return searchField === "alias" ? fieldValue === nextPathSegment : namesEqual(fieldValue, nextPathSegment);
1275
+ });
1210
1276
  if (!foundItem) {
1211
1277
  throw new Error(
1212
1278
  `Reference path "${ref}" not found: no item with ${searchField}="${nextPathSegment}" in "${pathParts.join(".")}". Referenced in ${context}.`
@@ -1612,12 +1678,12 @@ var CleanupManager = class {
1612
1678
  const configAssistantNames = new Set((config.resources.assistants || []).map(({ name }) => name));
1613
1679
  const configDatasourceNames = new Set((config.resources.datasources || []).map(({ name }) => name));
1614
1680
  const configWorkflowNames = new Set((config.resources.workflows || []).map(({ name }) => name));
1615
- const configSkillNames = new Set((config.resources.skills || []).map(({ name }) => name));
1681
+ const configSkillNames = new Set((config.resources.skills || []).map(({ name }) => name.toLowerCase()));
1616
1682
  return {
1617
1683
  assistants: managedResources.assistants.filter((name) => !configAssistantNames.has(name)),
1618
1684
  datasources: managedResources.datasources.filter((name) => !configDatasourceNames.has(name)),
1619
1685
  workflows: managedResources.workflows.filter((name) => !configWorkflowNames.has(name)),
1620
- skills: managedResources.skills.filter((name) => !configSkillNames.has(name))
1686
+ skills: managedResources.skills.filter((name) => !configSkillNames.has(name.toLowerCase()))
1621
1687
  };
1622
1688
  }
1623
1689
  /**
@@ -1886,6 +1952,62 @@ function checkSkillExists(client, name, stateManager) {
1886
1952
  );
1887
1953
  }
1888
1954
 
1955
+ // src/lib/resourceResolver.ts
1956
+ init_logger();
1957
+ function findSkillStateByName(stateManager, skillName) {
1958
+ const exactMatch = stateManager.getSkillState(skillName);
1959
+ if (exactMatch) {
1960
+ return exactMatch;
1961
+ }
1962
+ for (const name of stateManager.getAllManagedResources().skills) {
1963
+ if (namesEqual(name, skillName)) {
1964
+ return stateManager.getSkillState(name);
1965
+ }
1966
+ }
1967
+ return void 0;
1968
+ }
1969
+ function findConfiguredSkill(config, skillName) {
1970
+ return config.resources.skills?.find((skill) => namesEqual(skill.name, skillName));
1971
+ }
1972
+ function findImportedSkill(config, skillName) {
1973
+ return config.imported?.skills?.find((skill) => namesEqual(skill.name, skillName));
1974
+ }
1975
+ function lookupSkillId(skillName, stateManager, config) {
1976
+ const configuredSkill = findConfiguredSkill(config, skillName);
1977
+ const importedSkill = findImportedSkill(config, skillName);
1978
+ if (configuredSkill && importedSkill) {
1979
+ return void 0;
1980
+ }
1981
+ if (importedSkill) {
1982
+ return importedSkill.id;
1983
+ }
1984
+ if (!configuredSkill) {
1985
+ return void 0;
1986
+ }
1987
+ return findSkillStateByName(stateManager, configuredSkill.name)?.id;
1988
+ }
1989
+ function resolveSkillId(skillName, stateManager, config) {
1990
+ const configuredSkill = findConfiguredSkill(config, skillName);
1991
+ const importedSkill = findImportedSkill(config, skillName);
1992
+ if (configuredSkill && importedSkill) {
1993
+ throw new Error(
1994
+ `Skill "${skillName}" is defined in both resources.skills and imported.skills. Keep project-owned skills in resources.skills or external skills in imported.skills, not both.`
1995
+ );
1996
+ }
1997
+ const skillId = lookupSkillId(skillName, stateManager, config);
1998
+ if (skillId) {
1999
+ const source = importedSkill ? "imported" : "state";
2000
+ logger.info(` \u2713 Resolved${source === "imported" ? " (imported)" : ""} "${skillName}" \u2192 ${skillId}`);
2001
+ return skillId;
2002
+ }
2003
+ throw new Error(
2004
+ `Skill "${skillName}" cannot be resolved. For project-owned skills, define it in resources.skills and deploy it so state.json contains its ID. For external skills, add name and id to imported.skills.`
2005
+ );
2006
+ }
2007
+ function resolveSkillIds(skillNames, stateManager, config) {
2008
+ return skillNames.map((name) => resolveSkillId(name, stateManager, config));
2009
+ }
2010
+
1889
2011
  // src/deploy.ts
1890
2012
  function sortAssistantsByDependencies(assistants) {
1891
2013
  const sorted = [];
@@ -2029,16 +2151,7 @@ async function deployAssistants(config, client, loader, stateManager) {
2029
2151
  let resolvedSkillIds = [];
2030
2152
  if (assistant.skills && assistant.skills.length > 0) {
2031
2153
  logger.info(` Resolving ${assistant.skills.length} skill name(s)...`);
2032
- const resolvedIds = [];
2033
- for (const skillName of assistant.skills) {
2034
- const skillState = stateManager.getSkillState(skillName);
2035
- if (!skillState) {
2036
- throw new Error(`Skill "${skillName}" not found in state. Ensure the skill is deployed first.`);
2037
- }
2038
- resolvedIds.push(skillState.id);
2039
- logger.info(` \u2713 Resolved "${skillName}" \u2192 ${skillState.id}`);
2040
- }
2041
- resolvedSkillIds = resolvedIds;
2154
+ resolvedSkillIds = resolveSkillIds(assistant.skills, stateManager, config);
2042
2155
  }
2043
2156
  const assistantWithResolved = {
2044
2157
  ...assistant,
@@ -2158,9 +2271,8 @@ async function deployAssistants(config, client, loader, stateManager) {
2158
2271
  logger.error(` ${error.message}`);
2159
2272
  logger.debug(` Stack:`, error.stack);
2160
2273
  if ("statusCode" in error) {
2161
- const apiError = error;
2162
- logger.error(` Status: ${apiError.statusCode}`);
2163
- logger.error(` Data: ${JSON.stringify(apiError.response, null, 2)}`);
2274
+ logger.error(` Status: ${String(error.statusCode)}`);
2275
+ logger.error(` Data: ${JSON.stringify("response" in error ? error.response : void 0, null, 2)}`);
2164
2276
  } else if ("response" in error) {
2165
2277
  const axiosError = error;
2166
2278
  logger.error(` Status: ${axiosError.response?.status}`);
@@ -2607,6 +2719,13 @@ async function deployResources(options) {
2607
2719
  }
2608
2720
  if (orphaned.skills.length > 0) {
2609
2721
  logger.info(` \u2022 ${orphaned.skills.length} skill(s)`);
2722
+ const importedSkillNames = new Set(config.imported?.skills?.map((skill) => skill.name.toLowerCase()) || []);
2723
+ const migratedImportedSkills = orphaned.skills.filter((name) => importedSkillNames.has(name.toLowerCase()));
2724
+ if (migratedImportedSkills.length > 0) {
2725
+ logger.info(
2726
+ ` \u26A0\uFE0F ${migratedImportedSkills.length} skill(s) also exist in imported.skills. Run with --prune to remove stale IaC-managed skill state and delete old managed skill(s) from platform.`
2727
+ );
2728
+ }
2610
2729
  }
2611
2730
  if (process.env.SAMPLE_DEPLOY === "1") {
2612
2731
  logger.info("\n\u{1F50E} SAMPLE_DEPLOY=1 -> Skipping orphan deletion (simulation / partial deploy mode)\n");
@@ -2804,6 +2923,32 @@ var DependencyValidator = class {
2804
2923
  }
2805
2924
  return errors;
2806
2925
  }
2926
+ /**
2927
+ * Validate assistant skill references.
2928
+ * Check that all skill names point to resources.skills or imported.skills entries.
2929
+ */
2930
+ static validateAssistantSkillReferences(assistants, managedSkills, importedSkills) {
2931
+ const errors = [];
2932
+ for (const managedSkill of managedSkills) {
2933
+ const importedSkill = importedSkills.find((skill) => namesEqual(skill.name, managedSkill.name));
2934
+ if (importedSkill) {
2935
+ errors.push(
2936
+ `Skill "${managedSkill.name}" is defined in both resources.skills and imported.skills. Keep project-owned skills in resources.skills or external skills in imported.skills, not both.`
2937
+ );
2938
+ }
2939
+ }
2940
+ const isSkillAvailable = (skillName) => managedSkills.some((skill) => namesEqual(skill.name, skillName)) || importedSkills.some((skill) => namesEqual(skill.name, skillName));
2941
+ for (const assistant of assistants) {
2942
+ for (const skillName of assistant.skills || []) {
2943
+ if (!isSkillAvailable(skillName)) {
2944
+ errors.push(
2945
+ `Assistant "${assistant.name}": Skill reference "${skillName}" not found in resources.skills or imported.skills`
2946
+ );
2947
+ }
2948
+ }
2949
+ }
2950
+ return errors;
2951
+ }
2807
2952
  };
2808
2953
 
2809
2954
  // src/validate.ts
@@ -2887,6 +3032,12 @@ async function validateConfig(options) {
2887
3032
  if (config.resources.assistants && config.resources.assistants.length > 0) {
2888
3033
  const dependencyErrors = DependencyValidator.validateAssistantDependencies(config.resources.assistants);
2889
3034
  errors.push(...dependencyErrors);
3035
+ const skillReferenceErrors = DependencyValidator.validateAssistantSkillReferences(
3036
+ config.resources.assistants,
3037
+ config.resources.skills || [],
3038
+ config.imported?.skills || []
3039
+ );
3040
+ errors.push(...skillReferenceErrors);
2890
3041
  }
2891
3042
  if (config.resources.workflows && config.resources.workflows.length > 0) {
2892
3043
  const workflowsWithContent = config.resources.workflows.map((workflow) => {
@@ -3021,7 +3172,7 @@ async function previewSkills(skills, loader, stateManager, client) {
3021
3172
  }
3022
3173
  return changes;
3023
3174
  }
3024
- async function previewAssistants(assistants, loader, stateManager, client) {
3175
+ async function previewAssistants(assistants, loader, stateManager, client, config) {
3025
3176
  const changes = [];
3026
3177
  for (const assistant of assistants) {
3027
3178
  const promptContent = loader.loadPrompt(assistant.prompt);
@@ -3038,17 +3189,16 @@ async function previewAssistants(assistants, loader, stateManager, client) {
3038
3189
  async () => {
3039
3190
  const existingState = stateManager.getAssistantState(assistant.name);
3040
3191
  const configChanged = existingState ? existingState.promptChecksum !== calculateChecksum(promptContent) || existingState.configChecksum !== configChecksum : false;
3041
- if (configChanged) {
3042
- return {
3043
- hasChanged: true,
3044
- createDetails: `Model: ${assistant.model}`,
3045
- updateDetails: "Prompt or configuration changed"
3046
- };
3047
- }
3048
- if (existingState?.id && assistant.skills !== void 0) {
3049
- const desiredSkillIds = resolveDesiredSkillIds(assistant.skills, stateManager);
3050
- const drifted = await checkSkillAttachmentDrift(client, existingState.id, desiredSkillIds);
3051
- if (drifted) {
3192
+ if (assistant.skills !== void 0) {
3193
+ const { ids: desiredSkillIds, unresolved } = resolveDesiredSkillIds(assistant.skills, stateManager, config);
3194
+ if (unresolved.length > 0) {
3195
+ return {
3196
+ hasChanged: true,
3197
+ createDetails: `Unresolved skill reference(s): ${unresolved.join(", ")}`,
3198
+ updateDetails: `Unresolved skill reference(s): ${unresolved.join(", ")}`
3199
+ };
3200
+ }
3201
+ if (existingState?.id && await checkSkillAttachmentDrift(client, existingState.id, desiredSkillIds)) {
3052
3202
  return {
3053
3203
  hasChanged: true,
3054
3204
  createDetails: `Model: ${assistant.model}`,
@@ -3056,6 +3206,13 @@ async function previewAssistants(assistants, loader, stateManager, client) {
3056
3206
  };
3057
3207
  }
3058
3208
  }
3209
+ if (configChanged) {
3210
+ return {
3211
+ hasChanged: true,
3212
+ createDetails: `Model: ${assistant.model}`,
3213
+ updateDetails: "Prompt or configuration changed"
3214
+ };
3215
+ }
3059
3216
  return {
3060
3217
  hasChanged: false,
3061
3218
  createDetails: `Model: ${assistant.model}`
@@ -3066,15 +3223,18 @@ async function previewAssistants(assistants, loader, stateManager, client) {
3066
3223
  }
3067
3224
  return changes;
3068
3225
  }
3069
- function resolveDesiredSkillIds(skillNames, stateManager) {
3226
+ function resolveDesiredSkillIds(skillNames, stateManager, config) {
3070
3227
  const ids = [];
3228
+ const unresolved = [];
3071
3229
  for (const name of skillNames) {
3072
- const skillState = stateManager.getSkillState(name);
3073
- if (skillState) {
3074
- ids.push(skillState.id);
3230
+ const skillId = lookupSkillId(name, stateManager, config);
3231
+ if (skillId) {
3232
+ ids.push(skillId);
3233
+ } else {
3234
+ unresolved.push(name);
3075
3235
  }
3076
3236
  }
3077
- return ids;
3237
+ return { ids, unresolved };
3078
3238
  }
3079
3239
  async function checkSkillAttachmentDrift(client, assistantId, desiredSkillIds) {
3080
3240
  try {
@@ -3178,7 +3338,7 @@ async function previewChanges(appConfig, existingClient) {
3178
3338
  changes.push(...skillChanges);
3179
3339
  }
3180
3340
  if (config.resources.assistants) {
3181
- const assistantChanges = await previewAssistants(config.resources.assistants, loader, stateManager, client);
3341
+ const assistantChanges = await previewAssistants(config.resources.assistants, loader, stateManager, client, config);
3182
3342
  changes.push(...assistantChanges);
3183
3343
  }
3184
3344
  if (config.resources.datasources) {
@@ -3205,6 +3365,10 @@ async function previewChanges(appConfig, existingClient) {
3205
3365
  };
3206
3366
  }
3207
3367
 
3368
+ // src/backup.ts
3369
+ init_backupTransformers();
3370
+ init_skillUtils();
3371
+
3208
3372
  // src/lib/constants.ts
3209
3373
  var PAGINATION = {
3210
3374
  DEFAULT_PAGE_SIZE: 100};
@@ -3464,6 +3628,12 @@ function generateCodemieYaml(backup, projectName, backupDir, integrationSpecPath
3464
3628
  const integrationIdToAlias = /* @__PURE__ */ new Map();
3465
3629
  const integrationArray = [];
3466
3630
  const skillIdToName = new Map(backup.resources.skills.map((skill) => [skill.id, skill.name]));
3631
+ const referencedSkillIds = collectReferencedSkillIds(backup.resources.assistants);
3632
+ const { ownedSkills, importedSkills } = partitionSkillsForBackupYaml(
3633
+ backup.resources.skills,
3634
+ projectName,
3635
+ referencedSkillIds
3636
+ );
3467
3637
  for (const integration of backup.resources.integrations) {
3468
3638
  const credentialType = integration.credential_type || "integration";
3469
3639
  const alias = integration.alias || `${String(credentialType).toLowerCase()}_${integration.id.slice(0, 8)}`;
@@ -3509,6 +3679,7 @@ function generateCodemieYaml(backup, projectName, backupDir, integrationSpecPath
3509
3679
  imported: {
3510
3680
  assistants: [],
3511
3681
  datasources: [],
3682
+ skills: importedSkills.map((skill) => prepareImportedSkillForYaml(skill)),
3512
3683
  integrations: integrationArray
3513
3684
  },
3514
3685
  datasource_defaults: {
@@ -3530,8 +3701,8 @@ function generateCodemieYaml(backup, projectName, backupDir, integrationSpecPath
3530
3701
  workflows: backup.resources.workflows.map(
3531
3702
  (workflow) => prepareWorkflowForYaml(workflow, backup.state, backup.resources.assistants, backupDir)
3532
3703
  ),
3533
- ...backup.resources.skills.length > 0 && {
3534
- skills: backup.resources.skills.map((skill) => prepareSkillForYaml(skill, backup.state))
3704
+ ...ownedSkills.length > 0 && {
3705
+ skills: ownedSkills.map((skill) => prepareSkillForYaml(skill, backup.state))
3535
3706
  }
3536
3707
  }
3537
3708
  };
@@ -3664,6 +3835,27 @@ async function* streamResources(fetchPage, resourceType) {
3664
3835
  }
3665
3836
  }
3666
3837
  }
3838
+ function createFallbackImportedSkillDetail(skill) {
3839
+ return {
3840
+ id: skill.id,
3841
+ name: skill.name,
3842
+ description: "",
3843
+ project: skill.project || "",
3844
+ visibility: SkillVisibility.PUBLIC,
3845
+ created_by: null,
3846
+ categories: [],
3847
+ createdDate: "",
3848
+ updatedDate: null,
3849
+ is_attached: true,
3850
+ assistants_count: 0,
3851
+ user_abilities: [],
3852
+ unique_likes_count: 0,
3853
+ unique_dislikes_count: 0,
3854
+ content: "",
3855
+ toolkits: [],
3856
+ mcp_servers: []
3857
+ };
3858
+ }
3667
3859
  async function saveAssistantToBackup(assistant, client, backupData, backupDir) {
3668
3860
  logger.info(` \u2022 ${assistant.name} (${assistant.id})`);
3669
3861
  const full = await withTimeout(
@@ -3856,16 +4048,24 @@ async function backupIntegrations(client, backupData, projectName) {
3856
4048
  logger.info(`\u2713 Backed up ${projectIntegrations.length + userIntegrations.length} integration(s)
3857
4049
  `);
3858
4050
  }
3859
- async function backupSkills(client, backupData, backupDir, transaction) {
4051
+ async function backupSkills(client, backupData, backupDir, transaction, projectName) {
3860
4052
  logger.info("\u{1F9E0} Fetching skills...");
4053
+ const referencedSkillIds = collectReferencedSkillIds(backupData.resources.assistants);
3861
4054
  const allSkills = [];
3862
4055
  for await (const skill of streamResources((params) => client.skills.list(params), "skills")) {
3863
4056
  allSkills.push(skill);
3864
4057
  }
3865
- logger.info(` Found ${allSkills.length} skill(s)`);
3866
- transaction.setTotal("skills", allSkills.length);
4058
+ const skillsToBackup = allSkills.filter(
4059
+ (skill) => referencedSkillIds.has(skill.id) || !skill.project || isProjectOwnedSkill({ project: skill.project }, projectName)
4060
+ );
4061
+ const skippedExternalSkills = allSkills.length - skillsToBackup.length;
4062
+ logger.info(` Found ${allSkills.length} skill(s), backing up ${skillsToBackup.length}`);
4063
+ if (skippedExternalSkills > 0) {
4064
+ logger.info(` \u21B7 Skipping ${skippedExternalSkills} unreferenced external skill(s)`);
4065
+ }
4066
+ transaction.setTotal("skills", skillsToBackup.length);
3867
4067
  const limit = createConcurrentLimiter();
3868
- for (const skill of allSkills) {
4068
+ for (const skill of skillsToBackup) {
3869
4069
  if (transaction.isCompleted("skills", skill.id)) {
3870
4070
  logger.info(` \u21B7 Skipping ${skill.name} (already backed up)`);
3871
4071
  continue;
@@ -3874,16 +4074,37 @@ async function backupSkills(client, backupData, backupDir, transaction) {
3874
4074
  await limit(
3875
4075
  () => withRetry(async () => {
3876
4076
  logger.info(` \u2022 ${skill.name} (${skill.id})`);
3877
- const full = await withTimeout(
3878
- client.skills.get(skill.id),
3879
- TIMEOUTS_MS.SKILL_FETCH,
3880
- `Timeout fetching skill ${skill.id}`
3881
- );
4077
+ const listedAsProjectOwned = skill.project ? isProjectOwnedSkill({ project: skill.project }, projectName) : false;
4078
+ let full;
4079
+ try {
4080
+ full = await withTimeout(
4081
+ client.skills.get(skill.id),
4082
+ TIMEOUTS_MS.SKILL_FETCH,
4083
+ `Timeout fetching skill ${skill.id}`
4084
+ );
4085
+ } catch (error) {
4086
+ if (listedAsProjectOwned || !referencedSkillIds.has(skill.id)) {
4087
+ throw error;
4088
+ }
4089
+ logger.warn(
4090
+ ` \u26A0\uFE0F Failed to fetch imported skill details for ${skill.name}. Using list response as fallback: ${error instanceof Error ? error.message : String(error)}`
4091
+ );
4092
+ full = createFallbackImportedSkillDetail(skill);
4093
+ }
4094
+ const fullIsProjectOwned = isProjectOwnedSkill(full, projectName);
4095
+ if (!fullIsProjectOwned && !referencedSkillIds.has(skill.id)) {
4096
+ logger.info(` \u21B7 Skipping unreferenced external skill ${skill.name} (owner: ${full.project})`);
4097
+ return;
4098
+ }
3882
4099
  backupData.resources.skills.push(full);
3883
- const fileName = `${sanitizeFileName(skill.name)}.skill.md`;
3884
- const filePath = path.join(backupDir, "skills", fileName);
3885
- ensureDirectoryExists(filePath);
3886
- fs.writeFileSync(filePath, full.content || "", "utf8");
4100
+ if (fullIsProjectOwned) {
4101
+ const fileName = `${sanitizeFileName(skill.name)}.skill.md`;
4102
+ const filePath = path.join(backupDir, "skills", fileName);
4103
+ ensureDirectoryExists(filePath);
4104
+ fs.writeFileSync(filePath, full.content || "", "utf8");
4105
+ } else {
4106
+ logger.info(` \u21B7 Skipping content file for imported skill ${skill.name} (owner: ${full.project})`);
4107
+ }
3887
4108
  }, `Backup skill ${skill.name}`)
3888
4109
  );
3889
4110
  transaction.markCompleted("skills", skill.id);
@@ -3897,7 +4118,7 @@ async function backupSkills(client, backupData, backupDir, transaction) {
3897
4118
  transaction.markFailed("skills", skill.id, errorDetails.message);
3898
4119
  }
3899
4120
  }
3900
- logger.info(`\u2713 Backed up ${transaction.getData().resources.skills.completed.length} skill(s)
4121
+ logger.info(`\u2713 Backed up ${backupData.resources.skills.length} skill(s)
3901
4122
  `);
3902
4123
  }
3903
4124
  function getUniqueBackupDir(baseDir, timestamp) {
@@ -3982,11 +4203,11 @@ async function backupResources(options) {
3982
4203
  }
3983
4204
  }
3984
4205
  };
3985
- await backupSkills(client, backupData, tempBackupDir, transaction);
3986
4206
  await backupIntegrations(client, backupData, config.project.name);
3987
4207
  await backupDatasources(client, backupData, transaction, config.project.name);
3988
4208
  await backupWorkflows(client, backupData, tempBackupDir, transaction);
3989
4209
  await backupAssistants(client, backupData, tempBackupDir, transaction);
4210
+ await backupSkills(client, backupData, tempBackupDir, transaction, config.project.name);
3990
4211
  const stats = transaction.getData();
3991
4212
  const totalFailed = stats.resources.assistants.failed.length + stats.resources.datasources.failed.length + stats.resources.workflows.failed.length + stats.resources.skills.failed.length;
3992
4213
  if (totalFailed > 0) {
@@ -4034,6 +4255,7 @@ async function backupResources(options) {
4034
4255
 
4035
4256
  // src/import.ts
4036
4257
  init_converters();
4258
+ init_skillUtils();
4037
4259
  init_fileUtils();
4038
4260
  init_logger();
4039
4261
  init_checksumUtils();
@@ -4125,7 +4347,14 @@ Please use a more specific name.`);
4125
4347
  function checkResourceExists2(config, resourceType, name) {
4126
4348
  const pluralType = resourceType === "assistant" ? "assistants" : resourceType === "datasource" ? "datasources" : resourceType === "workflow" ? "workflows" : "skills";
4127
4349
  const resources = config.resources[pluralType] || [];
4128
- return resources.some((r) => r.name.toLowerCase() === name.toLowerCase());
4350
+ if (resources.some((r) => namesEqual(r.name, name))) {
4351
+ return true;
4352
+ }
4353
+ if (resourceType === "skill") {
4354
+ const importedSkills = config.imported?.skills || [];
4355
+ return importedSkills.some((skill) => namesEqual(skill.name, name));
4356
+ }
4357
+ return false;
4129
4358
  }
4130
4359
  function writeResourceFiles(rootDir, resourceType, resource, apiResponse) {
4131
4360
  const safeName = sanitizeFileName(resource.name);
@@ -4216,6 +4445,53 @@ function addImportToCodemieYaml(rootDir, codemieConfigPath, resourceType, relati
4216
4445
  fs.writeFileSync(configPath, doc.toString(), "utf8");
4217
4446
  logger.info(` \u2705 Added $import to ${codemieConfigPath} \u2192 resources.${pluralType}`);
4218
4447
  }
4448
+ function addImportedSkillToCodemieYaml(rootDir, codemieConfigPath, skill) {
4449
+ const configPath = path.join(rootDir, codemieConfigPath);
4450
+ if (!fs.existsSync(configPath)) {
4451
+ throw new Error(`Configuration file not found: ${configPath}`);
4452
+ }
4453
+ const content = fs.readFileSync(configPath, "utf8");
4454
+ const doc = yaml6.parseDocument(content);
4455
+ let importedNode = doc.get("imported");
4456
+ if (!importedNode) {
4457
+ importedNode = doc.createNode({ assistants: [], datasources: [], skills: [], integrations: [] });
4458
+ doc.set("imported", importedNode);
4459
+ }
4460
+ let skillsArray = doc.getIn(["imported", "skills"]);
4461
+ if (!skillsArray) {
4462
+ skillsArray = doc.createNode([]);
4463
+ doc.setIn(["imported", "skills"], skillsArray);
4464
+ }
4465
+ const existingSkill = skillsArray.items.find((item) => {
4466
+ if (!yaml6.isMap(item)) {
4467
+ return false;
4468
+ }
4469
+ const existingName = item.get("name");
4470
+ return typeof existingName === "string" && namesEqual(existingName, skill.name);
4471
+ });
4472
+ if (existingSkill) {
4473
+ const existingId = existingSkill.get("id");
4474
+ if (existingId && existingId !== skill.id) {
4475
+ throw new Error(
4476
+ `Skill "${skill.name}" already exists in imported.skills with a different ID (${existingId}). Remove the existing entry or use the intended skill ID.`
4477
+ );
4478
+ }
4479
+ logger.info(` \u21B7 Skill "${skill.name}" already exists in imported.skills`);
4480
+ return;
4481
+ }
4482
+ skillsArray.add(doc.createNode({ name: skill.name, id: skill.id }));
4483
+ fs.writeFileSync(configPath, doc.toString(), "utf8");
4484
+ logger.info(` \u2705 Added skill to ${codemieConfigPath} \u2192 imported.skills`);
4485
+ }
4486
+ function ensureExternalSkillDoesNotConflictWithManagedSkill(config, skillName) {
4487
+ const managedSkill = config.resources.skills?.find((skill) => namesEqual(skill.name, skillName));
4488
+ if (!managedSkill) {
4489
+ return;
4490
+ }
4491
+ throw new Error(
4492
+ `External skill "${skillName}" is already defined in resources.skills. Keep project-owned skills in resources.skills or external skills in imported.skills, not both.`
4493
+ );
4494
+ }
4219
4495
  function updateState(stateManager, resourceType, resource, apiResponse) {
4220
4496
  switch (resourceType) {
4221
4497
  case "assistant": {
@@ -4303,16 +4579,22 @@ async function importResource(options) {
4303
4579
  logger.info(` Resolving ${assistantData.skill_ids.length} skill ID(s)...`);
4304
4580
  const skillNames = [];
4305
4581
  for (const skillId of assistantData.skill_ids) {
4582
+ let skill;
4306
4583
  try {
4307
- const skill = await withTimeout(
4584
+ skill = await withTimeout(
4308
4585
  client.skills.get(skillId),
4309
4586
  TIMEOUTS_MS.ASSISTANT_FETCH,
4310
4587
  `Timeout fetching skill "${skillId}"`
4311
4588
  );
4312
- skillNames.push(skill.name);
4313
- logger.info(` \u2713 Resolved skill ${skillId.slice(0, 8)}... \u2192 "${skill.name}"`);
4314
4589
  } catch {
4315
4590
  logger.warn(` \u26A0\uFE0F Could not resolve skill ID "${skillId}" \u2014 skipping`);
4591
+ continue;
4592
+ }
4593
+ skillNames.push(skill.name);
4594
+ logger.info(` \u2713 Resolved skill ${skillId.slice(0, 8)}... \u2192 "${skill.name}"`);
4595
+ if (!isProjectOwnedSkill(skill, config.project.name)) {
4596
+ ensureExternalSkillDoesNotConflictWithManagedSkill(config, skill.name);
4597
+ addImportedSkillToCodemieYaml(rootDir, appConfig.codemieConfig, { name: skill.name, id: skill.id });
4316
4598
  }
4317
4599
  }
4318
4600
  if (skillNames.length > 0) {
@@ -4348,9 +4630,22 @@ async function importResource(options) {
4348
4630
  logger.info(` Searching for skill "${slug}"...`);
4349
4631
  const skill = await findSkill(client, slug);
4350
4632
  logger.info(` \u2713 Found: ${skill.name} (${skill.id})`);
4351
- apiResponse = skill;
4352
- resource = skillResponseToResource(skill);
4353
- break;
4633
+ if (skill.name !== slug && checkResourceExists2(config, resourceType, skill.name)) {
4634
+ throw new Error(
4635
+ `A ${resourceType} named "${skill.name}" already exists in the configuration. Remove it first or use a different name.`
4636
+ );
4637
+ }
4638
+ if (isProjectOwnedSkill(skill, config.project.name)) {
4639
+ logger.info(` Project-owned skill \u2014 importing into resources.skills`);
4640
+ apiResponse = skill;
4641
+ resource = skillResponseToResource(skill);
4642
+ break;
4643
+ }
4644
+ addImportedSkillToCodemieYaml(rootDir, appConfig.codemieConfig, { name: skill.name, id: skill.id });
4645
+ logger.info(`
4646
+ \u2705 Successfully imported external skill "${skill.name}" into imported.skills
4647
+ `);
4648
+ return;
4354
4649
  }
4355
4650
  }
4356
4651
  const relativeResourcePath = writeResourceFiles(rootDir, resourceType, resource, apiResponse);
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@codemieai/cdk",
3
- "version": "0.1.543",
3
+ "version": "0.1.544",
4
4
  "type": "module",
5
5
  "description": "Infrastructure as Code solution for managing Codemie AI assistants, datasources, and workflows through declarative YAML configuration",
6
6
  "main": "./dist/index.js",