@codemieai/cdk 0.1.543 → 0.1.545

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/cli/index.js CHANGED
@@ -1,10 +1,10 @@
1
1
  #!/usr/bin/env node
2
+ import ora from 'ora';
3
+ import * as crypto from 'crypto';
4
+ import { DataSourceType, CodeMieClient, SkillVisibility } from 'codemie-sdk';
2
5
  import * as fs6 from 'fs';
3
6
  import * as path6 from 'path';
4
7
  import path6__default from 'path';
5
- import ora from 'ora';
6
- import * as crypto2 from 'crypto';
7
- import { DataSourceType, CodeMieClient } from 'codemie-sdk';
8
8
  import * as yaml5 from 'yaml';
9
9
  import { program } from '@commander-js/extra-typings';
10
10
  import { z } from 'zod';
@@ -21,66 +21,6 @@ var __export = (target, all) => {
21
21
  for (var name in all)
22
22
  __defProp(target, name, { get: all[name], enumerable: true });
23
23
  };
24
- function sanitizeFileName(name, maxLength = 255) {
25
- const nameWithHyphens = name.replaceAll(/[/\\]/g, "-");
26
- const sanitized = nameWithHyphens.toLowerCase().replaceAll(/[^a-z0-9]+/g, "-").replaceAll(/^-+|-+$/g, "").slice(0, maxLength);
27
- if (!sanitized) {
28
- throw new Error(`Sanitized filename is empty for input: "${name}"`);
29
- }
30
- return sanitized;
31
- }
32
- function validateBackupDirectory(backupDir, minSpaceGB = 1) {
33
- try {
34
- const parentDir = path6.dirname(backupDir);
35
- if (!fs6.existsSync(parentDir)) {
36
- fs6.mkdirSync(parentDir, { recursive: true });
37
- }
38
- const testFile = path6.join(parentDir, ".write-test");
39
- fs6.writeFileSync(testFile, "test");
40
- fs6.unlinkSync(testFile);
41
- try {
42
- const stats = fs6.statfsSync(parentDir);
43
- const availableGB = stats.bavail * stats.bsize / BYTES_IN_GB;
44
- if (availableGB < minSpaceGB) {
45
- throw new Error(`Insufficient disk space: ${availableGB.toFixed(2)}GB available, need ${minSpaceGB}GB`);
46
- }
47
- } catch (error) {
48
- if (error.code !== "ERR_METHOD_NOT_SUPPORTED") {
49
- throw error;
50
- }
51
- }
52
- } catch (error) {
53
- throw new Error(`Cannot write to backup directory: ${error instanceof Error ? error.message : String(error)}`);
54
- }
55
- }
56
- function moveAtomically(tempPath, finalPath) {
57
- try {
58
- fs6.renameSync(tempPath, finalPath);
59
- } catch (error) {
60
- const err = error;
61
- if (err.code === "EEXIST") {
62
- throw new Error(`Destination already exists: ${finalPath}`);
63
- }
64
- throw error;
65
- }
66
- }
67
- function cleanupDirectory(dirPath) {
68
- if (fs6.existsSync(dirPath)) {
69
- fs6.rmSync(dirPath, { recursive: true, force: true });
70
- }
71
- }
72
- function ensureDirectoryExists(filePath) {
73
- const dir = path6.dirname(filePath);
74
- if (!fs6.existsSync(dir)) {
75
- fs6.mkdirSync(dir, { recursive: true });
76
- }
77
- }
78
- var BYTES_IN_GB;
79
- var init_fileUtils = __esm({
80
- "src/lib/fileUtils.ts"() {
81
- BYTES_IN_GB = 1024 ** 3;
82
- }
83
- });
84
24
  function formatDuration(durationMs) {
85
25
  if (durationMs < 1e3) {
86
26
  return `${durationMs}ms`;
@@ -354,7 +294,7 @@ function calculateChecksum(content) {
354
294
  if (content.length === 0) {
355
295
  logger.warn("\u26A0\uFE0F Calculating checksum of empty string");
356
296
  }
357
- return crypto2.createHash("sha256").update(content, "utf8").digest("hex");
297
+ return crypto.createHash("sha256").update(content, "utf8").digest("hex");
358
298
  }
359
299
  function normalizeAssistantConfig(assistant, buildConfig = null) {
360
300
  return {
@@ -750,12 +690,84 @@ var init_converters = __esm({
750
690
  FILE_SUMMARY_ALIASES = /* @__PURE__ */ new Set(["file_summary", "file-summary", "file-summay"]);
751
691
  }
752
692
  });
693
+ function sanitizeFileName(name, maxLength = 255) {
694
+ const nameWithHyphens = name.replaceAll(/[/\\]/g, "-");
695
+ const sanitized = nameWithHyphens.toLowerCase().replaceAll(/[^a-z0-9]+/g, "-").replaceAll(/^-+|-+$/g, "").slice(0, maxLength);
696
+ if (!sanitized) {
697
+ throw new Error(`Sanitized filename is empty for input: "${name}"`);
698
+ }
699
+ return sanitized;
700
+ }
701
+ function validateBackupDirectory(backupDir, minSpaceGB = 1) {
702
+ try {
703
+ const parentDir = path6.dirname(backupDir);
704
+ if (!fs6.existsSync(parentDir)) {
705
+ fs6.mkdirSync(parentDir, { recursive: true });
706
+ }
707
+ const testFile = path6.join(parentDir, ".write-test");
708
+ fs6.writeFileSync(testFile, "test");
709
+ fs6.unlinkSync(testFile);
710
+ try {
711
+ const stats = fs6.statfsSync(parentDir);
712
+ const availableGB = stats.bavail * stats.bsize / BYTES_IN_GB;
713
+ if (availableGB < minSpaceGB) {
714
+ throw new Error(`Insufficient disk space: ${availableGB.toFixed(2)}GB available, need ${minSpaceGB}GB`);
715
+ }
716
+ } catch (error) {
717
+ if (error.code !== "ERR_METHOD_NOT_SUPPORTED") {
718
+ throw error;
719
+ }
720
+ }
721
+ } catch (error) {
722
+ throw new Error(`Cannot write to backup directory: ${error instanceof Error ? error.message : String(error)}`);
723
+ }
724
+ }
725
+ function moveAtomically(tempPath, finalPath) {
726
+ try {
727
+ fs6.renameSync(tempPath, finalPath);
728
+ } catch (error) {
729
+ const err = error;
730
+ if (err.code === "EEXIST") {
731
+ throw new Error(`Destination already exists: ${finalPath}`);
732
+ }
733
+ throw error;
734
+ }
735
+ }
736
+ function cleanupDirectory(dirPath) {
737
+ if (fs6.existsSync(dirPath)) {
738
+ fs6.rmSync(dirPath, { recursive: true, force: true });
739
+ }
740
+ }
741
+ function ensureDirectoryExists(filePath) {
742
+ const dir = path6.dirname(filePath);
743
+ if (!fs6.existsSync(dir)) {
744
+ fs6.mkdirSync(dir, { recursive: true });
745
+ }
746
+ }
747
+ var BYTES_IN_GB;
748
+ var init_fileUtils = __esm({
749
+ "src/lib/fileUtils.ts"() {
750
+ BYTES_IN_GB = 1024 ** 3;
751
+ }
752
+ });
753
+
754
+ // src/lib/skillUtils.ts
755
+ function isProjectOwnedSkill(skill, projectName) {
756
+ return skill.project.trim().toLowerCase() === projectName.trim().toLowerCase();
757
+ }
758
+ var init_skillUtils = __esm({
759
+ "src/lib/skillUtils.ts"() {
760
+ }
761
+ });
753
762
 
754
763
  // src/lib/backupTransformers.ts
755
764
  var backupTransformers_exports = {};
756
765
  __export(backupTransformers_exports, {
766
+ collectReferencedSkillIds: () => collectReferencedSkillIds,
767
+ partitionSkillsForBackupYaml: () => partitionSkillsForBackupYaml,
757
768
  prepareAssistantForYaml: () => prepareAssistantForYaml,
758
769
  prepareDatasourceForYaml: () => prepareDatasourceForYaml,
770
+ prepareImportedSkillForYaml: () => prepareImportedSkillForYaml,
759
771
  prepareSkillForYaml: () => prepareSkillForYaml,
760
772
  prepareWorkflowForYaml: () => prepareWorkflowForYaml,
761
773
  transformMcpServer: () => transformMcpServer,
@@ -912,6 +924,36 @@ function prepareWorkflowForYaml(workflow, state, assistants, backupDir) {
912
924
  };
913
925
  return resource;
914
926
  }
927
+ function collectReferencedSkillIds(assistants) {
928
+ const referencedSkillIds = /* @__PURE__ */ new Set();
929
+ for (const assistant of assistants) {
930
+ const assistantData = assistant;
931
+ for (const skillId of assistantData.skill_ids || []) {
932
+ referencedSkillIds.add(skillId);
933
+ }
934
+ }
935
+ return referencedSkillIds;
936
+ }
937
+ function partitionSkillsForBackupYaml(skills, projectName, referencedSkillIds) {
938
+ const ownedSkills = [];
939
+ const importedSkills = [];
940
+ for (const skill of skills) {
941
+ if (isProjectOwnedSkill(skill, projectName)) {
942
+ ownedSkills.push(skill);
943
+ continue;
944
+ }
945
+ if (referencedSkillIds.has(skill.id)) {
946
+ importedSkills.push(skill);
947
+ }
948
+ }
949
+ return { ownedSkills, importedSkills };
950
+ }
951
+ function prepareImportedSkillForYaml(skill) {
952
+ return {
953
+ name: skill.name,
954
+ id: skill.id
955
+ };
956
+ }
915
957
  function prepareSkillForYaml(skill, state) {
916
958
  const resource = skillResponseToResource(skill);
917
959
  if (!state.resources.skills) {
@@ -932,13 +974,14 @@ var init_backupTransformers = __esm({
932
974
  init_converters();
933
975
  init_fileUtils();
934
976
  init_logger();
977
+ init_skillUtils();
935
978
  init_typeGuards();
936
979
  }
937
980
  });
938
981
 
939
982
  // package.json
940
983
  var package_default = {
941
- version: "0.1.543"};
984
+ version: "0.1.545"};
942
985
  var appConfigSchema = z.object({
943
986
  rootDir: z.string(),
944
987
  codemieConfig: z.string(),
@@ -999,6 +1042,10 @@ function removeEmptyFields(userConfig) {
999
1042
  );
1000
1043
  }
1001
1044
 
1045
+ // src/backup.ts
1046
+ init_backupTransformers();
1047
+ init_skillUtils();
1048
+
1002
1049
  // src/lib/constants.ts
1003
1050
  var PAGINATION = {
1004
1051
  DEFAULT_PAGE_SIZE: 100};
@@ -1101,7 +1148,7 @@ var BackupTransaction = class {
1101
1148
  }
1102
1149
  }
1103
1150
  generateTransactionId() {
1104
- return crypto2.randomBytes(8).toString("hex");
1151
+ return crypto.randomBytes(8).toString("hex");
1105
1152
  }
1106
1153
  /**
1107
1154
  * Save transaction state to disk (checkpoint) with timeout protection
@@ -1258,6 +1305,12 @@ function generateCodemieYaml(backup, projectName, backupDir, integrationSpecPath
1258
1305
  const integrationIdToAlias = /* @__PURE__ */ new Map();
1259
1306
  const integrationArray = [];
1260
1307
  const skillIdToName = new Map(backup.resources.skills.map((skill) => [skill.id, skill.name]));
1308
+ const referencedSkillIds = collectReferencedSkillIds(backup.resources.assistants);
1309
+ const { ownedSkills, importedSkills } = partitionSkillsForBackupYaml(
1310
+ backup.resources.skills,
1311
+ projectName,
1312
+ referencedSkillIds
1313
+ );
1261
1314
  for (const integration of backup.resources.integrations) {
1262
1315
  const credentialType = integration.credential_type || "integration";
1263
1316
  const alias = integration.alias || `${String(credentialType).toLowerCase()}_${integration.id.slice(0, 8)}`;
@@ -1303,6 +1356,7 @@ function generateCodemieYaml(backup, projectName, backupDir, integrationSpecPath
1303
1356
  imported: {
1304
1357
  assistants: [],
1305
1358
  datasources: [],
1359
+ skills: importedSkills.map((skill) => prepareImportedSkillForYaml(skill)),
1306
1360
  integrations: integrationArray
1307
1361
  },
1308
1362
  datasource_defaults: {
@@ -1324,8 +1378,8 @@ function generateCodemieYaml(backup, projectName, backupDir, integrationSpecPath
1324
1378
  workflows: backup.resources.workflows.map(
1325
1379
  (workflow) => prepareWorkflowForYaml(workflow, backup.state, backup.resources.assistants, backupDir)
1326
1380
  ),
1327
- ...backup.resources.skills.length > 0 && {
1328
- skills: backup.resources.skills.map((skill) => prepareSkillForYaml(skill, backup.state))
1381
+ ...ownedSkills.length > 0 && {
1382
+ skills: ownedSkills.map((skill) => prepareSkillForYaml(skill, backup.state))
1329
1383
  }
1330
1384
  }
1331
1385
  };
@@ -1405,6 +1459,13 @@ async function createClient(config) {
1405
1459
  }
1406
1460
  return client;
1407
1461
  }
1462
+
1463
+ // src/lib/nameUtils.ts
1464
+ function namesEqual(a, b) {
1465
+ return a.toLowerCase() === b.toLowerCase();
1466
+ }
1467
+
1468
+ // src/lib/codemieConfigLoader.ts
1408
1469
  var CodemieConfigLoader = class {
1409
1470
  appConfig;
1410
1471
  constructor(appConfig) {
@@ -1499,6 +1560,14 @@ var CodemieConfigLoader = class {
1499
1560
  }
1500
1561
  }
1501
1562
  }
1563
+ for (const skill of config.imported?.skills || []) {
1564
+ if (!skill.name) {
1565
+ errors.push("Imported skill entry is missing required field: name");
1566
+ }
1567
+ if (!skill.id) {
1568
+ errors.push(`Imported skill "${skill.name || "<unknown>"}" is missing required field: id`);
1569
+ }
1570
+ }
1502
1571
  return {
1503
1572
  valid: errors.length === 0,
1504
1573
  errors
@@ -1713,6 +1782,7 @@ Import chain: ${[...visitedFiles].join(" \u2192 ")} \u2192 ${normalizedPath}`
1713
1782
  * - imported.integrations (array): searches by 'alias' field
1714
1783
  * - imported.assistants (array): searches by 'name' field
1715
1784
  * - imported.datasources (array): searches by 'name' field
1785
+ * - imported.skills (array): searches by 'name' field
1716
1786
  */
1717
1787
  resolveReference(config, ref, context) {
1718
1788
  const parts = ref.split(".");
@@ -1736,9 +1806,16 @@ Import chain: ${[...visitedFiles].join(" \u2192 ")} \u2192 ${normalizedPath}`
1736
1806
  if (Array.isArray(current) && hasMorePathSegments) {
1737
1807
  const nextPathSegment = parts[i + 1];
1738
1808
  const searchField = part === "integrations" ? "alias" : "name";
1739
- const foundItem = current.find(
1740
- (item) => typeof item === "object" && item !== null && item[searchField] === nextPathSegment
1741
- );
1809
+ const foundItem = current.find((item) => {
1810
+ if (typeof item !== "object" || item === null) {
1811
+ return false;
1812
+ }
1813
+ const fieldValue = item[searchField];
1814
+ if (typeof fieldValue !== "string") {
1815
+ return false;
1816
+ }
1817
+ return searchField === "alias" ? fieldValue === nextPathSegment : namesEqual(fieldValue, nextPathSegment);
1818
+ });
1742
1819
  if (!foundItem) {
1743
1820
  throw new Error(
1744
1821
  `Reference path "${ref}" not found: no item with ${searchField}="${nextPathSegment}" in "${pathParts.join(".")}". Referenced in ${context}.`
@@ -1895,6 +1972,27 @@ async function* streamResources(fetchPage, resourceType) {
1895
1972
  }
1896
1973
  }
1897
1974
  }
1975
+ function createFallbackImportedSkillDetail(skill) {
1976
+ return {
1977
+ id: skill.id,
1978
+ name: skill.name,
1979
+ description: "",
1980
+ project: skill.project || "",
1981
+ visibility: SkillVisibility.PUBLIC,
1982
+ created_by: null,
1983
+ categories: [],
1984
+ createdDate: "",
1985
+ updatedDate: null,
1986
+ is_attached: true,
1987
+ assistants_count: 0,
1988
+ user_abilities: [],
1989
+ unique_likes_count: 0,
1990
+ unique_dislikes_count: 0,
1991
+ content: "",
1992
+ toolkits: [],
1993
+ mcp_servers: []
1994
+ };
1995
+ }
1898
1996
  async function saveAssistantToBackup(assistant, client, backupData, backupDir) {
1899
1997
  logger.info(` \u2022 ${assistant.name} (${assistant.id})`);
1900
1998
  const full = await withTimeout(
@@ -2087,16 +2185,24 @@ async function backupIntegrations(client, backupData, projectName) {
2087
2185
  logger.info(`\u2713 Backed up ${projectIntegrations.length + userIntegrations.length} integration(s)
2088
2186
  `);
2089
2187
  }
2090
- async function backupSkills(client, backupData, backupDir, transaction) {
2188
+ async function backupSkills(client, backupData, backupDir, transaction, projectName) {
2091
2189
  logger.info("\u{1F9E0} Fetching skills...");
2190
+ const referencedSkillIds = collectReferencedSkillIds(backupData.resources.assistants);
2092
2191
  const allSkills = [];
2093
2192
  for await (const skill of streamResources((params) => client.skills.list(params), "skills")) {
2094
2193
  allSkills.push(skill);
2095
2194
  }
2096
- logger.info(` Found ${allSkills.length} skill(s)`);
2097
- transaction.setTotal("skills", allSkills.length);
2195
+ const skillsToBackup = allSkills.filter(
2196
+ (skill) => referencedSkillIds.has(skill.id) || !skill.project || isProjectOwnedSkill({ project: skill.project }, projectName)
2197
+ );
2198
+ const skippedExternalSkills = allSkills.length - skillsToBackup.length;
2199
+ logger.info(` Found ${allSkills.length} skill(s), backing up ${skillsToBackup.length}`);
2200
+ if (skippedExternalSkills > 0) {
2201
+ logger.info(` \u21B7 Skipping ${skippedExternalSkills} unreferenced external skill(s)`);
2202
+ }
2203
+ transaction.setTotal("skills", skillsToBackup.length);
2098
2204
  const limit = createConcurrentLimiter();
2099
- for (const skill of allSkills) {
2205
+ for (const skill of skillsToBackup) {
2100
2206
  if (transaction.isCompleted("skills", skill.id)) {
2101
2207
  logger.info(` \u21B7 Skipping ${skill.name} (already backed up)`);
2102
2208
  continue;
@@ -2105,16 +2211,37 @@ async function backupSkills(client, backupData, backupDir, transaction) {
2105
2211
  await limit(
2106
2212
  () => withRetry(async () => {
2107
2213
  logger.info(` \u2022 ${skill.name} (${skill.id})`);
2108
- const full = await withTimeout(
2109
- client.skills.get(skill.id),
2110
- TIMEOUTS_MS.SKILL_FETCH,
2111
- `Timeout fetching skill ${skill.id}`
2112
- );
2214
+ const listedAsProjectOwned = skill.project ? isProjectOwnedSkill({ project: skill.project }, projectName) : false;
2215
+ let full;
2216
+ try {
2217
+ full = await withTimeout(
2218
+ client.skills.get(skill.id),
2219
+ TIMEOUTS_MS.SKILL_FETCH,
2220
+ `Timeout fetching skill ${skill.id}`
2221
+ );
2222
+ } catch (error) {
2223
+ if (listedAsProjectOwned || !referencedSkillIds.has(skill.id)) {
2224
+ throw error;
2225
+ }
2226
+ logger.warn(
2227
+ ` \u26A0\uFE0F Failed to fetch imported skill details for ${skill.name}. Using list response as fallback: ${error instanceof Error ? error.message : String(error)}`
2228
+ );
2229
+ full = createFallbackImportedSkillDetail(skill);
2230
+ }
2231
+ const fullIsProjectOwned = isProjectOwnedSkill(full, projectName);
2232
+ if (!fullIsProjectOwned && !referencedSkillIds.has(skill.id)) {
2233
+ logger.info(` \u21B7 Skipping unreferenced external skill ${skill.name} (owner: ${full.project})`);
2234
+ return;
2235
+ }
2113
2236
  backupData.resources.skills.push(full);
2114
- const fileName = `${sanitizeFileName(skill.name)}.skill.md`;
2115
- const filePath = path6.join(backupDir, "skills", fileName);
2116
- ensureDirectoryExists(filePath);
2117
- fs6.writeFileSync(filePath, full.content || "", "utf8");
2237
+ if (fullIsProjectOwned) {
2238
+ const fileName = `${sanitizeFileName(skill.name)}.skill.md`;
2239
+ const filePath = path6.join(backupDir, "skills", fileName);
2240
+ ensureDirectoryExists(filePath);
2241
+ fs6.writeFileSync(filePath, full.content || "", "utf8");
2242
+ } else {
2243
+ logger.info(` \u21B7 Skipping content file for imported skill ${skill.name} (owner: ${full.project})`);
2244
+ }
2118
2245
  }, `Backup skill ${skill.name}`)
2119
2246
  );
2120
2247
  transaction.markCompleted("skills", skill.id);
@@ -2128,7 +2255,7 @@ async function backupSkills(client, backupData, backupDir, transaction) {
2128
2255
  transaction.markFailed("skills", skill.id, errorDetails.message);
2129
2256
  }
2130
2257
  }
2131
- logger.info(`\u2713 Backed up ${transaction.getData().resources.skills.completed.length} skill(s)
2258
+ logger.info(`\u2713 Backed up ${backupData.resources.skills.length} skill(s)
2132
2259
  `);
2133
2260
  }
2134
2261
  function getUniqueBackupDir(baseDir, timestamp) {
@@ -2213,11 +2340,11 @@ async function backupResources(options) {
2213
2340
  }
2214
2341
  }
2215
2342
  };
2216
- await backupSkills(client, backupData, tempBackupDir, transaction);
2217
2343
  await backupIntegrations(client, backupData, config.project.name);
2218
2344
  await backupDatasources(client, backupData, transaction, config.project.name);
2219
2345
  await backupWorkflows(client, backupData, tempBackupDir, transaction);
2220
2346
  await backupAssistants(client, backupData, tempBackupDir, transaction);
2347
+ await backupSkills(client, backupData, tempBackupDir, transaction, config.project.name);
2221
2348
  const stats = transaction.getData();
2222
2349
  const totalFailed = stats.resources.assistants.failed.length + stats.resources.datasources.failed.length + stats.resources.workflows.failed.length + stats.resources.skills.failed.length;
2223
2350
  if (totalFailed > 0) {
@@ -2403,12 +2530,12 @@ var CleanupManager = class {
2403
2530
  const configAssistantNames = new Set((config.resources.assistants || []).map(({ name }) => name));
2404
2531
  const configDatasourceNames = new Set((config.resources.datasources || []).map(({ name }) => name));
2405
2532
  const configWorkflowNames = new Set((config.resources.workflows || []).map(({ name }) => name));
2406
- const configSkillNames = new Set((config.resources.skills || []).map(({ name }) => name));
2533
+ const configSkillNames = new Set((config.resources.skills || []).map(({ name }) => name.toLowerCase()));
2407
2534
  return {
2408
2535
  assistants: managedResources.assistants.filter((name) => !configAssistantNames.has(name)),
2409
2536
  datasources: managedResources.datasources.filter((name) => !configDatasourceNames.has(name)),
2410
2537
  workflows: managedResources.workflows.filter((name) => !configWorkflowNames.has(name)),
2411
- skills: managedResources.skills.filter((name) => !configSkillNames.has(name))
2538
+ skills: managedResources.skills.filter((name) => !configSkillNames.has(name.toLowerCase()))
2412
2539
  };
2413
2540
  }
2414
2541
  /**
@@ -2595,6 +2722,62 @@ function checkSkillExists(client, name, stateManager) {
2595
2722
  );
2596
2723
  }
2597
2724
 
2725
+ // src/lib/resourceResolver.ts
2726
+ init_logger();
2727
+ function findSkillStateByName(stateManager, skillName) {
2728
+ const exactMatch = stateManager.getSkillState(skillName);
2729
+ if (exactMatch) {
2730
+ return exactMatch;
2731
+ }
2732
+ for (const name of stateManager.getAllManagedResources().skills) {
2733
+ if (namesEqual(name, skillName)) {
2734
+ return stateManager.getSkillState(name);
2735
+ }
2736
+ }
2737
+ return void 0;
2738
+ }
2739
+ function findConfiguredSkill(config, skillName) {
2740
+ return config.resources.skills?.find((skill) => namesEqual(skill.name, skillName));
2741
+ }
2742
+ function findImportedSkill(config, skillName) {
2743
+ return config.imported?.skills?.find((skill) => namesEqual(skill.name, skillName));
2744
+ }
2745
+ function lookupSkillId(skillName, stateManager, config) {
2746
+ const configuredSkill = findConfiguredSkill(config, skillName);
2747
+ const importedSkill = findImportedSkill(config, skillName);
2748
+ if (configuredSkill && importedSkill) {
2749
+ return void 0;
2750
+ }
2751
+ if (importedSkill) {
2752
+ return importedSkill.id;
2753
+ }
2754
+ if (!configuredSkill) {
2755
+ return void 0;
2756
+ }
2757
+ return findSkillStateByName(stateManager, configuredSkill.name)?.id;
2758
+ }
2759
+ function resolveSkillId(skillName, stateManager, config) {
2760
+ const configuredSkill = findConfiguredSkill(config, skillName);
2761
+ const importedSkill = findImportedSkill(config, skillName);
2762
+ if (configuredSkill && importedSkill) {
2763
+ throw new Error(
2764
+ `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.`
2765
+ );
2766
+ }
2767
+ const skillId = lookupSkillId(skillName, stateManager, config);
2768
+ if (skillId) {
2769
+ const source = importedSkill ? "imported" : "state";
2770
+ logger.info(` \u2713 Resolved${source === "imported" ? " (imported)" : ""} "${skillName}" \u2192 ${skillId}`);
2771
+ return skillId;
2772
+ }
2773
+ throw new Error(
2774
+ `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.`
2775
+ );
2776
+ }
2777
+ function resolveSkillIds(skillNames, stateManager, config) {
2778
+ return skillNames.map((name) => resolveSkillId(name, stateManager, config));
2779
+ }
2780
+
2598
2781
  // src/lib/stateManager.ts
2599
2782
  init_checksumUtils();
2600
2783
  init_codemieConfigChecksums();
@@ -2990,16 +3173,7 @@ async function deployAssistants(config, client, loader, stateManager) {
2990
3173
  let resolvedSkillIds = [];
2991
3174
  if (assistant.skills && assistant.skills.length > 0) {
2992
3175
  logger.info(` Resolving ${assistant.skills.length} skill name(s)...`);
2993
- const resolvedIds = [];
2994
- for (const skillName of assistant.skills) {
2995
- const skillState = stateManager.getSkillState(skillName);
2996
- if (!skillState) {
2997
- throw new Error(`Skill "${skillName}" not found in state. Ensure the skill is deployed first.`);
2998
- }
2999
- resolvedIds.push(skillState.id);
3000
- logger.info(` \u2713 Resolved "${skillName}" \u2192 ${skillState.id}`);
3001
- }
3002
- resolvedSkillIds = resolvedIds;
3176
+ resolvedSkillIds = resolveSkillIds(assistant.skills, stateManager, config);
3003
3177
  }
3004
3178
  const assistantWithResolved = {
3005
3179
  ...assistant,
@@ -3119,9 +3293,8 @@ async function deployAssistants(config, client, loader, stateManager) {
3119
3293
  logger.error(` ${error.message}`);
3120
3294
  logger.debug(` Stack:`, error.stack);
3121
3295
  if ("statusCode" in error) {
3122
- const apiError = error;
3123
- logger.error(` Status: ${apiError.statusCode}`);
3124
- logger.error(` Data: ${JSON.stringify(apiError.response, null, 2)}`);
3296
+ logger.error(` Status: ${String(error.statusCode)}`);
3297
+ logger.error(` Data: ${JSON.stringify("response" in error ? error.response : void 0, null, 2)}`);
3125
3298
  } else if ("response" in error) {
3126
3299
  const axiosError = error;
3127
3300
  logger.error(` Status: ${axiosError.response?.status}`);
@@ -3568,6 +3741,13 @@ async function deployResources(options) {
3568
3741
  }
3569
3742
  if (orphaned.skills.length > 0) {
3570
3743
  logger.info(` \u2022 ${orphaned.skills.length} skill(s)`);
3744
+ const importedSkillNames = new Set(config.imported?.skills?.map((skill) => skill.name.toLowerCase()) || []);
3745
+ const migratedImportedSkills = orphaned.skills.filter((name) => importedSkillNames.has(name.toLowerCase()));
3746
+ if (migratedImportedSkills.length > 0) {
3747
+ logger.info(
3748
+ ` \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.`
3749
+ );
3750
+ }
3571
3751
  }
3572
3752
  if (process.env.SAMPLE_DEPLOY === "1") {
3573
3753
  logger.info("\n\u{1F50E} SAMPLE_DEPLOY=1 -> Skipping orphan deletion (simulation / partial deploy mode)\n");
@@ -3772,6 +3952,7 @@ async function main3(options) {
3772
3952
 
3773
3953
  // src/import.ts
3774
3954
  init_converters();
3955
+ init_skillUtils();
3775
3956
  init_fileUtils();
3776
3957
  init_logger();
3777
3958
  init_checksumUtils();
@@ -3863,7 +4044,14 @@ Please use a more specific name.`);
3863
4044
  function checkResourceExists2(config, resourceType, name) {
3864
4045
  const pluralType = resourceType === "assistant" ? "assistants" : resourceType === "datasource" ? "datasources" : resourceType === "workflow" ? "workflows" : "skills";
3865
4046
  const resources = config.resources[pluralType] || [];
3866
- return resources.some((r) => r.name.toLowerCase() === name.toLowerCase());
4047
+ if (resources.some((r) => namesEqual(r.name, name))) {
4048
+ return true;
4049
+ }
4050
+ if (resourceType === "skill") {
4051
+ const importedSkills = config.imported?.skills || [];
4052
+ return importedSkills.some((skill) => namesEqual(skill.name, name));
4053
+ }
4054
+ return false;
3867
4055
  }
3868
4056
  function writeResourceFiles(rootDir, resourceType, resource, apiResponse) {
3869
4057
  const safeName = sanitizeFileName(resource.name);
@@ -3954,6 +4142,53 @@ function addImportToCodemieYaml(rootDir, codemieConfigPath, resourceType, relati
3954
4142
  fs6.writeFileSync(configPath, doc.toString(), "utf8");
3955
4143
  logger.info(` \u2705 Added $import to ${codemieConfigPath} \u2192 resources.${pluralType}`);
3956
4144
  }
4145
+ function addImportedSkillToCodemieYaml(rootDir, codemieConfigPath, skill) {
4146
+ const configPath = path6.join(rootDir, codemieConfigPath);
4147
+ if (!fs6.existsSync(configPath)) {
4148
+ throw new Error(`Configuration file not found: ${configPath}`);
4149
+ }
4150
+ const content = fs6.readFileSync(configPath, "utf8");
4151
+ const doc = yaml5.parseDocument(content);
4152
+ let importedNode = doc.get("imported");
4153
+ if (!importedNode) {
4154
+ importedNode = doc.createNode({ assistants: [], datasources: [], skills: [], integrations: [] });
4155
+ doc.set("imported", importedNode);
4156
+ }
4157
+ let skillsArray = doc.getIn(["imported", "skills"]);
4158
+ if (!skillsArray) {
4159
+ skillsArray = doc.createNode([]);
4160
+ doc.setIn(["imported", "skills"], skillsArray);
4161
+ }
4162
+ const existingSkill = skillsArray.items.find((item) => {
4163
+ if (!yaml5.isMap(item)) {
4164
+ return false;
4165
+ }
4166
+ const existingName = item.get("name");
4167
+ return typeof existingName === "string" && namesEqual(existingName, skill.name);
4168
+ });
4169
+ if (existingSkill) {
4170
+ const existingId = existingSkill.get("id");
4171
+ if (existingId && existingId !== skill.id) {
4172
+ throw new Error(
4173
+ `Skill "${skill.name}" already exists in imported.skills with a different ID (${existingId}). Remove the existing entry or use the intended skill ID.`
4174
+ );
4175
+ }
4176
+ logger.info(` \u21B7 Skill "${skill.name}" already exists in imported.skills`);
4177
+ return;
4178
+ }
4179
+ skillsArray.add(doc.createNode({ name: skill.name, id: skill.id }));
4180
+ fs6.writeFileSync(configPath, doc.toString(), "utf8");
4181
+ logger.info(` \u2705 Added skill to ${codemieConfigPath} \u2192 imported.skills`);
4182
+ }
4183
+ function ensureExternalSkillDoesNotConflictWithManagedSkill(config, skillName) {
4184
+ const managedSkill = config.resources.skills?.find((skill) => namesEqual(skill.name, skillName));
4185
+ if (!managedSkill) {
4186
+ return;
4187
+ }
4188
+ throw new Error(
4189
+ `External skill "${skillName}" is already defined in resources.skills. Keep project-owned skills in resources.skills or external skills in imported.skills, not both.`
4190
+ );
4191
+ }
3957
4192
  function updateState(stateManager, resourceType, resource, apiResponse) {
3958
4193
  switch (resourceType) {
3959
4194
  case "assistant": {
@@ -4041,16 +4276,22 @@ async function importResource(options) {
4041
4276
  logger.info(` Resolving ${assistantData.skill_ids.length} skill ID(s)...`);
4042
4277
  const skillNames = [];
4043
4278
  for (const skillId of assistantData.skill_ids) {
4279
+ let skill;
4044
4280
  try {
4045
- const skill = await withTimeout(
4281
+ skill = await withTimeout(
4046
4282
  client.skills.get(skillId),
4047
4283
  TIMEOUTS_MS.ASSISTANT_FETCH,
4048
4284
  `Timeout fetching skill "${skillId}"`
4049
4285
  );
4050
- skillNames.push(skill.name);
4051
- logger.info(` \u2713 Resolved skill ${skillId.slice(0, 8)}... \u2192 "${skill.name}"`);
4052
4286
  } catch {
4053
4287
  logger.warn(` \u26A0\uFE0F Could not resolve skill ID "${skillId}" \u2014 skipping`);
4288
+ continue;
4289
+ }
4290
+ skillNames.push(skill.name);
4291
+ logger.info(` \u2713 Resolved skill ${skillId.slice(0, 8)}... \u2192 "${skill.name}"`);
4292
+ if (!isProjectOwnedSkill(skill, config.project.name)) {
4293
+ ensureExternalSkillDoesNotConflictWithManagedSkill(config, skill.name);
4294
+ addImportedSkillToCodemieYaml(rootDir, appConfig.codemieConfig, { name: skill.name, id: skill.id });
4054
4295
  }
4055
4296
  }
4056
4297
  if (skillNames.length > 0) {
@@ -4086,9 +4327,22 @@ async function importResource(options) {
4086
4327
  logger.info(` Searching for skill "${slug}"...`);
4087
4328
  const skill = await findSkill(client, slug);
4088
4329
  logger.info(` \u2713 Found: ${skill.name} (${skill.id})`);
4089
- apiResponse = skill;
4090
- resource = skillResponseToResource(skill);
4091
- break;
4330
+ if (skill.name !== slug && checkResourceExists2(config, resourceType, skill.name)) {
4331
+ throw new Error(
4332
+ `A ${resourceType} named "${skill.name}" already exists in the configuration. Remove it first or use a different name.`
4333
+ );
4334
+ }
4335
+ if (isProjectOwnedSkill(skill, config.project.name)) {
4336
+ logger.info(` Project-owned skill \u2014 importing into resources.skills`);
4337
+ apiResponse = skill;
4338
+ resource = skillResponseToResource(skill);
4339
+ break;
4340
+ }
4341
+ addImportedSkillToCodemieYaml(rootDir, appConfig.codemieConfig, { name: skill.name, id: skill.id });
4342
+ logger.info(`
4343
+ \u2705 Successfully imported external skill "${skill.name}" into imported.skills
4344
+ `);
4345
+ return;
4092
4346
  }
4093
4347
  }
4094
4348
  const relativeResourcePath = writeResourceFiles(rootDir, resourceType, resource, apiResponse);
@@ -4418,7 +4672,7 @@ async function previewSkills(skills, loader, stateManager, client) {
4418
4672
  }
4419
4673
  return changes;
4420
4674
  }
4421
- async function previewAssistants(assistants, loader, stateManager, client) {
4675
+ async function previewAssistants(assistants, loader, stateManager, client, config) {
4422
4676
  const changes = [];
4423
4677
  for (const assistant of assistants) {
4424
4678
  const promptContent = loader.loadPrompt(assistant.prompt);
@@ -4435,17 +4689,16 @@ async function previewAssistants(assistants, loader, stateManager, client) {
4435
4689
  async () => {
4436
4690
  const existingState = stateManager.getAssistantState(assistant.name);
4437
4691
  const configChanged = existingState ? existingState.promptChecksum !== calculateChecksum(promptContent) || existingState.configChecksum !== configChecksum : false;
4438
- if (configChanged) {
4439
- return {
4440
- hasChanged: true,
4441
- createDetails: `Model: ${assistant.model}`,
4442
- updateDetails: "Prompt or configuration changed"
4443
- };
4444
- }
4445
- if (existingState?.id && assistant.skills !== void 0) {
4446
- const desiredSkillIds = resolveDesiredSkillIds(assistant.skills, stateManager);
4447
- const drifted = await checkSkillAttachmentDrift(client, existingState.id, desiredSkillIds);
4448
- if (drifted) {
4692
+ if (assistant.skills !== void 0) {
4693
+ const { ids: desiredSkillIds, unresolved } = resolveDesiredSkillIds(assistant.skills, stateManager, config);
4694
+ if (unresolved.length > 0) {
4695
+ return {
4696
+ hasChanged: true,
4697
+ createDetails: `Unresolved skill reference(s): ${unresolved.join(", ")}`,
4698
+ updateDetails: `Unresolved skill reference(s): ${unresolved.join(", ")}`
4699
+ };
4700
+ }
4701
+ if (existingState?.id && await checkSkillAttachmentDrift(client, existingState.id, desiredSkillIds)) {
4449
4702
  return {
4450
4703
  hasChanged: true,
4451
4704
  createDetails: `Model: ${assistant.model}`,
@@ -4453,6 +4706,13 @@ async function previewAssistants(assistants, loader, stateManager, client) {
4453
4706
  };
4454
4707
  }
4455
4708
  }
4709
+ if (configChanged) {
4710
+ return {
4711
+ hasChanged: true,
4712
+ createDetails: `Model: ${assistant.model}`,
4713
+ updateDetails: "Prompt or configuration changed"
4714
+ };
4715
+ }
4456
4716
  return {
4457
4717
  hasChanged: false,
4458
4718
  createDetails: `Model: ${assistant.model}`
@@ -4463,15 +4723,18 @@ async function previewAssistants(assistants, loader, stateManager, client) {
4463
4723
  }
4464
4724
  return changes;
4465
4725
  }
4466
- function resolveDesiredSkillIds(skillNames, stateManager) {
4726
+ function resolveDesiredSkillIds(skillNames, stateManager, config) {
4467
4727
  const ids = [];
4728
+ const unresolved = [];
4468
4729
  for (const name of skillNames) {
4469
- const skillState = stateManager.getSkillState(name);
4470
- if (skillState) {
4471
- ids.push(skillState.id);
4730
+ const skillId = lookupSkillId(name, stateManager, config);
4731
+ if (skillId) {
4732
+ ids.push(skillId);
4733
+ } else {
4734
+ unresolved.push(name);
4472
4735
  }
4473
4736
  }
4474
- return ids;
4737
+ return { ids, unresolved };
4475
4738
  }
4476
4739
  async function checkSkillAttachmentDrift(client, assistantId, desiredSkillIds) {
4477
4740
  try {
@@ -4575,7 +4838,7 @@ async function previewChanges(appConfig, existingClient) {
4575
4838
  changes.push(...skillChanges);
4576
4839
  }
4577
4840
  if (config.resources.assistants) {
4578
- const assistantChanges = await previewAssistants(config.resources.assistants, loader, stateManager, client);
4841
+ const assistantChanges = await previewAssistants(config.resources.assistants, loader, stateManager, client, config);
4579
4842
  changes.push(...assistantChanges);
4580
4843
  }
4581
4844
  if (config.resources.datasources) {
@@ -4809,6 +5072,32 @@ var DependencyValidator = class {
4809
5072
  }
4810
5073
  return errors;
4811
5074
  }
5075
+ /**
5076
+ * Validate assistant skill references.
5077
+ * Check that all skill names point to resources.skills or imported.skills entries.
5078
+ */
5079
+ static validateAssistantSkillReferences(assistants, managedSkills, importedSkills) {
5080
+ const errors = [];
5081
+ for (const managedSkill of managedSkills) {
5082
+ const importedSkill = importedSkills.find((skill) => namesEqual(skill.name, managedSkill.name));
5083
+ if (importedSkill) {
5084
+ errors.push(
5085
+ `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.`
5086
+ );
5087
+ }
5088
+ }
5089
+ const isSkillAvailable = (skillName) => managedSkills.some((skill) => namesEqual(skill.name, skillName)) || importedSkills.some((skill) => namesEqual(skill.name, skillName));
5090
+ for (const assistant of assistants) {
5091
+ for (const skillName of assistant.skills || []) {
5092
+ if (!isSkillAvailable(skillName)) {
5093
+ errors.push(
5094
+ `Assistant "${assistant.name}": Skill reference "${skillName}" not found in resources.skills or imported.skills`
5095
+ );
5096
+ }
5097
+ }
5098
+ }
5099
+ return errors;
5100
+ }
4812
5101
  };
4813
5102
 
4814
5103
  // src/validate.ts
@@ -4892,6 +5181,12 @@ async function validateConfig(options) {
4892
5181
  if (config.resources.assistants && config.resources.assistants.length > 0) {
4893
5182
  const dependencyErrors = DependencyValidator.validateAssistantDependencies(config.resources.assistants);
4894
5183
  errors.push(...dependencyErrors);
5184
+ const skillReferenceErrors = DependencyValidator.validateAssistantSkillReferences(
5185
+ config.resources.assistants,
5186
+ config.resources.skills || [],
5187
+ config.imported?.skills || []
5188
+ );
5189
+ errors.push(...skillReferenceErrors);
4895
5190
  }
4896
5191
  if (config.resources.workflows && config.resources.workflows.length > 0) {
4897
5192
  const workflowsWithContent = config.resources.workflows.map((workflow) => {