newo 1.9.1 → 2.0.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.
Files changed (66) hide show
  1. package/CHANGELOG.md +131 -0
  2. package/README.md +68 -20
  3. package/dist/cli/commands/conversations.d.ts +3 -0
  4. package/dist/cli/commands/conversations.js +38 -0
  5. package/dist/cli/commands/help.d.ts +5 -0
  6. package/dist/cli/commands/help.js +50 -0
  7. package/dist/cli/commands/import-akb.d.ts +3 -0
  8. package/dist/cli/commands/import-akb.js +62 -0
  9. package/dist/cli/commands/list-customers.d.ts +3 -0
  10. package/dist/cli/commands/list-customers.js +13 -0
  11. package/dist/cli/commands/meta.d.ts +3 -0
  12. package/dist/cli/commands/meta.js +19 -0
  13. package/dist/cli/commands/pull-attributes.d.ts +3 -0
  14. package/dist/cli/commands/pull-attributes.js +16 -0
  15. package/dist/cli/commands/pull.d.ts +3 -0
  16. package/dist/cli/commands/pull.js +34 -0
  17. package/dist/cli/commands/push.d.ts +3 -0
  18. package/dist/cli/commands/push.js +39 -0
  19. package/dist/cli/commands/status.d.ts +3 -0
  20. package/dist/cli/commands/status.js +22 -0
  21. package/dist/cli/customer-selection.d.ts +23 -0
  22. package/dist/cli/customer-selection.js +110 -0
  23. package/dist/cli/errors.d.ts +9 -0
  24. package/dist/cli/errors.js +111 -0
  25. package/dist/cli.js +66 -463
  26. package/dist/fsutil.js +1 -1
  27. package/dist/sync/attributes.d.ts +7 -0
  28. package/dist/sync/attributes.js +90 -0
  29. package/dist/sync/conversations.d.ts +7 -0
  30. package/dist/sync/conversations.js +218 -0
  31. package/dist/sync/metadata.d.ts +8 -0
  32. package/dist/sync/metadata.js +124 -0
  33. package/dist/sync/projects.d.ts +13 -0
  34. package/dist/sync/projects.js +283 -0
  35. package/dist/sync/push.d.ts +7 -0
  36. package/dist/sync/push.js +171 -0
  37. package/dist/sync/skill-files.d.ts +42 -0
  38. package/dist/sync/skill-files.js +121 -0
  39. package/dist/sync/status.d.ts +6 -0
  40. package/dist/sync/status.js +247 -0
  41. package/dist/sync.d.ts +10 -8
  42. package/dist/sync.js +12 -1197
  43. package/dist/types.d.ts +0 -1
  44. package/package.json +2 -2
  45. package/src/cli/commands/conversations.ts +47 -0
  46. package/src/cli/commands/help.ts +50 -0
  47. package/src/cli/commands/import-akb.ts +71 -0
  48. package/src/cli/commands/list-customers.ts +14 -0
  49. package/src/cli/commands/meta.ts +26 -0
  50. package/src/cli/commands/pull-attributes.ts +23 -0
  51. package/src/cli/commands/pull.ts +43 -0
  52. package/src/cli/commands/push.ts +47 -0
  53. package/src/cli/commands/status.ts +30 -0
  54. package/src/cli/customer-selection.ts +135 -0
  55. package/src/cli/errors.ts +111 -0
  56. package/src/cli.ts +77 -471
  57. package/src/fsutil.ts +1 -1
  58. package/src/sync/attributes.ts +110 -0
  59. package/src/sync/conversations.ts +257 -0
  60. package/src/sync/metadata.ts +153 -0
  61. package/src/sync/projects.ts +359 -0
  62. package/src/sync/push.ts +200 -0
  63. package/src/sync/skill-files.ts +176 -0
  64. package/src/sync/status.ts +277 -0
  65. package/src/sync.ts +14 -1389
  66. package/src/types.ts +0 -1
@@ -0,0 +1,359 @@
1
+ /**
2
+ * Project synchronization operations
3
+ */
4
+ import {
5
+ listProjects,
6
+ listAgents,
7
+ listFlowSkills,
8
+ listFlowEvents,
9
+ listFlowStates
10
+ } from '../api.js';
11
+ import {
12
+ ensureState,
13
+ writeFileSafe,
14
+ mapPath,
15
+ projectMetadataPath,
16
+ agentMetadataPath,
17
+ flowMetadataPath,
18
+ skillMetadataPath,
19
+ skillScriptPath,
20
+ skillFolderPath,
21
+ flowsYamlPath,
22
+ customerAttributesPath
23
+ } from '../fsutil.js';
24
+ import {
25
+ findSkillScriptFiles,
26
+ isContentDifferent,
27
+ askForOverwrite,
28
+ getExtensionForRunner
29
+ } from './skill-files.js';
30
+ import fs from 'fs-extra';
31
+ import { sha256, saveHashes } from '../hash.js';
32
+ import yaml from 'js-yaml';
33
+ import { generateFlowsYaml } from './metadata.js';
34
+ import { saveCustomerAttributes } from './attributes.js';
35
+ import type { AxiosInstance } from 'axios';
36
+ import type {
37
+ ProjectData,
38
+ ProjectMap,
39
+ LegacyProjectMap,
40
+ HashStore,
41
+ CustomerConfig,
42
+ ProjectMetadata,
43
+ AgentMetadata,
44
+ FlowMetadata,
45
+ SkillMetadata
46
+ } from '../types.js';
47
+
48
+ // Type guards for project map formats
49
+ export function isProjectMap(x: unknown): x is ProjectMap {
50
+ return typeof x === 'object' && x !== null && 'projects' in x;
51
+ }
52
+
53
+ export function isLegacyProjectMap(x: unknown): x is LegacyProjectMap {
54
+ return typeof x === 'object' && x !== null && 'projectId' in x && 'agents' in x;
55
+ }
56
+
57
+ /**
58
+ * Pull a single project and all its data
59
+ */
60
+ export async function pullSingleProject(
61
+ client: AxiosInstance,
62
+ customer: CustomerConfig,
63
+ projectId: string | null,
64
+ verbose: boolean = false,
65
+ silentOverwrite: boolean = false
66
+ ): Promise<void> {
67
+ if (verbose) console.log(`📋 Loading project list for customer ${customer.idn}...`);
68
+
69
+ const projects = projectId ?
70
+ [{ id: projectId, idn: 'unknown', title: 'Project' }] :
71
+ await listProjects(client);
72
+
73
+ if (projects.length === 0) {
74
+ console.log(`No projects found for customer ${customer.idn}`);
75
+ return;
76
+ }
77
+
78
+ await ensureState(customer.idn);
79
+
80
+ // Load existing mappings if they exist
81
+ let existingMap: ProjectMap = { projects: {} };
82
+ const mapFile = mapPath(customer.idn);
83
+ if (await fs.pathExists(mapFile)) {
84
+ const mapData = await fs.readJson(mapFile) as unknown;
85
+ if (isProjectMap(mapData)) {
86
+ existingMap = mapData;
87
+ } else if (isLegacyProjectMap(mapData)) {
88
+ // Convert legacy format to new format
89
+ existingMap = {
90
+ projects: {
91
+ [mapData.projectIdn || '']: mapData as ProjectData
92
+ }
93
+ };
94
+ }
95
+ }
96
+
97
+ const newHashes: HashStore = {};
98
+
99
+ // Progress tracking
100
+ let totalSkills = 0;
101
+ let processedSkills = 0;
102
+
103
+ // Count total skills for progress tracking
104
+ for (const project of projects) {
105
+ const agents = await listAgents(client, project.id);
106
+ for (const agent of agents) {
107
+ const flows = agent.flows || [];
108
+ for (const flow of flows) {
109
+ const skills = await listFlowSkills(client, flow.id);
110
+ totalSkills += skills.length;
111
+ }
112
+ }
113
+ }
114
+
115
+ if (verbose) console.log(`📊 Total skills to process: ${totalSkills}`);
116
+
117
+ for (const project of projects) {
118
+ if (verbose) console.log(`📁 Processing project: ${project.title} (${project.idn})`);
119
+
120
+ // Create project metadata
121
+ const projectMeta: ProjectMetadata = {
122
+ id: project.id,
123
+ idn: project.idn,
124
+ title: project.title,
125
+ description: project.description || '',
126
+ created_at: project.created_at || '',
127
+ updated_at: project.updated_at || ''
128
+ };
129
+
130
+ // Save project metadata
131
+ const projectMetaPath = projectMetadataPath(customer.idn, project.idn);
132
+ const projectMetaYaml = yaml.dump(projectMeta, { indent: 2, quotingType: '"', forceQuotes: false });
133
+ await writeFileSafe(projectMetaPath, projectMetaYaml);
134
+ newHashes[projectMetaPath] = sha256(projectMetaYaml);
135
+
136
+ const agents = await listAgents(client, project.id);
137
+ if (verbose) console.log(` 📋 Found ${agents.length} agents in project ${project.title}`);
138
+
139
+ const projectData: ProjectData = {
140
+ projectId: project.id,
141
+ projectIdn: project.idn,
142
+ agents: {}
143
+ };
144
+
145
+ for (const agent of agents) {
146
+ if (verbose) console.log(` 📁 Processing agent: ${agent.title} (${agent.idn})`);
147
+
148
+ // Create agent metadata
149
+ const agentMeta: AgentMetadata = {
150
+ id: agent.id,
151
+ idn: agent.idn,
152
+ title: agent.title || '',
153
+ description: agent.description || ''
154
+ };
155
+
156
+ // Save agent metadata
157
+ const agentMetaPath = agentMetadataPath(customer.idn, project.idn, agent.idn);
158
+ const agentMetaYaml = yaml.dump(agentMeta, { indent: 2, quotingType: '"', forceQuotes: false });
159
+ await writeFileSafe(agentMetaPath, agentMetaYaml);
160
+ newHashes[agentMetaPath] = sha256(agentMetaYaml);
161
+
162
+ projectData.agents[agent.idn] = {
163
+ id: agent.id,
164
+ flows: {}
165
+ };
166
+
167
+ const flows = agent.flows || [];
168
+ if (verbose && flows.length > 0) {
169
+ console.log(` 📋 Found ${flows.length} flows in agent ${agent.title}`);
170
+ }
171
+
172
+ for (const flow of flows) {
173
+ if (verbose) console.log(` 📁 Processing flow: ${flow.title} (${flow.idn})`);
174
+
175
+ // Get flow events and states for metadata
176
+ const [events, states] = await Promise.all([
177
+ listFlowEvents(client, flow.id).catch(() => []),
178
+ listFlowStates(client, flow.id).catch(() => [])
179
+ ]);
180
+
181
+ // Create flow metadata
182
+ const flowMeta: FlowMetadata = {
183
+ id: flow.id,
184
+ idn: flow.idn,
185
+ title: flow.title,
186
+ description: flow.description || '',
187
+ default_runner_type: flow.default_runner_type,
188
+ default_model: flow.default_model,
189
+ events,
190
+ state_fields: states
191
+ };
192
+
193
+ // Save flow metadata
194
+ const flowMetaPath = flowMetadataPath(customer.idn, project.idn, agent.idn, flow.idn);
195
+ const flowMetaYaml = yaml.dump(flowMeta, { indent: 2, quotingType: '"', forceQuotes: false });
196
+ await writeFileSafe(flowMetaPath, flowMetaYaml);
197
+ newHashes[flowMetaPath] = sha256(flowMetaYaml);
198
+
199
+ projectData.agents[agent.idn]!.flows[flow.idn] = {
200
+ id: flow.id,
201
+ skills: {}
202
+ };
203
+
204
+ const skills = await listFlowSkills(client, flow.id);
205
+ if (verbose) console.log(` 📋 Found ${skills.length} skills in flow ${flow.title}`);
206
+
207
+ for (const skill of skills) {
208
+ processedSkills++;
209
+ const progress = `[${processedSkills}/${totalSkills}]`;
210
+
211
+ if (verbose) {
212
+ console.log(` 📄 ${progress} Processing skill: ${skill.title} (${skill.idn})`);
213
+ } else {
214
+ // Show progress for non-verbose mode
215
+ if (processedSkills % 10 === 0 || processedSkills === totalSkills) {
216
+ process.stdout.write(`\r📄 Processing skills: ${processedSkills}/${totalSkills} (${Math.round(processedSkills/totalSkills*100)}%)`);
217
+ }
218
+ }
219
+
220
+ // Create skill metadata
221
+ const skillMeta: SkillMetadata = {
222
+ id: skill.id,
223
+ idn: skill.idn,
224
+ title: skill.title,
225
+ runner_type: skill.runner_type,
226
+ model: skill.model,
227
+ parameters: [...skill.parameters],
228
+ path: skill.path
229
+ };
230
+
231
+ // Save skill metadata
232
+ const skillMetaPath = skillMetadataPath(customer.idn, project.idn, agent.idn, flow.idn, skill.idn);
233
+ const skillMetaYaml = yaml.dump(skillMeta, { indent: 2, quotingType: '"', forceQuotes: false });
234
+ await writeFileSafe(skillMetaPath, skillMetaYaml);
235
+ newHashes[skillMetaPath] = sha256(skillMetaYaml);
236
+
237
+ // Handle skill script with IDN-based naming and overwrite detection
238
+ const scriptContent = skill.prompt_script || '';
239
+ const targetScriptPath = skillScriptPath(customer.idn, project.idn, agent.idn, flow.idn, skill.idn, skill.runner_type);
240
+ const folderPath = skillFolderPath(customer.idn, project.idn, agent.idn, flow.idn, skill.idn);
241
+
242
+ // Check for existing script files in the skill folder
243
+ const existingFiles = await findSkillScriptFiles(folderPath);
244
+ let shouldWrite = true;
245
+ let hasContentMatch = false;
246
+
247
+ if (existingFiles.length > 0) {
248
+ // Check if any existing file has the same content
249
+ hasContentMatch = existingFiles.some(file => !isContentDifferent(file.content, scriptContent));
250
+
251
+ if (hasContentMatch) {
252
+ // Content is the same - remove old files and write with correct IDN name
253
+ const matchingFile = existingFiles.find(file => !isContentDifferent(file.content, scriptContent));
254
+ const correctName = `${skill.idn}.${getExtensionForRunner(skill.runner_type)}`;
255
+
256
+ if (matchingFile && matchingFile.fileName !== correctName) {
257
+ // Remove old file and write with correct IDN-based name
258
+ await fs.remove(matchingFile.filePath);
259
+ if (verbose) console.log(` 🔄 Renamed ${matchingFile.fileName} → ${correctName}`);
260
+ } else if (matchingFile && matchingFile.fileName === correctName) {
261
+ // Already has correct name and content
262
+ shouldWrite = false;
263
+ newHashes[matchingFile.filePath] = sha256(scriptContent);
264
+ if (verbose) console.log(` ✓ Content unchanged for ${skill.idn}, keeping existing file`);
265
+ }
266
+ } else if (!silentOverwrite) {
267
+ // Content is different, ask for overwrite
268
+ const existingFile = existingFiles[0]!;
269
+ const shouldOverwrite = await askForOverwrite(
270
+ skill.idn,
271
+ existingFile.fileName,
272
+ `${skill.idn}.${getExtensionForRunner(skill.runner_type)}`
273
+ );
274
+
275
+ if (!shouldOverwrite) {
276
+ shouldWrite = false;
277
+ if (verbose) console.log(` ⚠️ Skipped overwrite for ${skill.idn}`);
278
+ } else {
279
+ // Remove existing files before writing new one
280
+ for (const file of existingFiles) {
281
+ await fs.remove(file.filePath);
282
+ if (verbose) console.log(` 🗑️ Removed ${file.fileName}`);
283
+ }
284
+ }
285
+ } else {
286
+ // Silent overwrite mode - remove existing files
287
+ for (const file of existingFiles) {
288
+ await fs.remove(file.filePath);
289
+ if (verbose) console.log(` 🔄 Silent overwrite: removed ${file.fileName}`);
290
+ }
291
+ }
292
+ }
293
+
294
+ if (shouldWrite) {
295
+ await writeFileSafe(targetScriptPath, scriptContent);
296
+ newHashes[targetScriptPath] = sha256(scriptContent);
297
+ const fileName = `${skill.idn}.${getExtensionForRunner(skill.runner_type)}`;
298
+ if (verbose) console.log(` ✓ Saved ${fileName}`);
299
+ }
300
+
301
+ projectData.agents[agent.idn]!.flows[flow.idn]!.skills[skill.idn] = skillMeta;
302
+ }
303
+ }
304
+ }
305
+
306
+ // Store project data in map
307
+ existingMap.projects[project.idn] = projectData;
308
+ }
309
+
310
+ // Clear progress line for non-verbose mode
311
+ if (!verbose && totalSkills > 0) {
312
+ console.log(`\n✅ Processed ${totalSkills} skills`);
313
+ }
314
+
315
+ // Save updated project map
316
+ await writeFileSafe(mapFile, JSON.stringify(existingMap, null, 2));
317
+
318
+ // Pull customer attributes as part of the project pull
319
+ try {
320
+ if (verbose) console.log(`🔍 Fetching customer attributes for ${customer.idn}...`);
321
+ const attributesContent = await saveCustomerAttributes(client, customer, verbose);
322
+
323
+ // Add attributes.yaml hash to the hash store
324
+ const attributesPath = customerAttributesPath(customer.idn);
325
+ newHashes[attributesPath] = sha256(attributesContent);
326
+
327
+ if (verbose) console.log(`✅ Customer attributes saved to newo_customers/${customer.idn}/attributes.yaml`);
328
+ } catch (error) {
329
+ console.warn(`⚠️ Failed to fetch customer attributes for ${customer.idn}: ${error instanceof Error ? error.message : String(error)}`);
330
+ if (verbose) console.warn('You can manually pull attributes using: newo pull-attributes');
331
+ }
332
+
333
+ // Generate flows.yaml and get its content for hashing
334
+ const flowsYamlContent = await generateFlowsYaml(existingMap, customer.idn, verbose);
335
+
336
+ // Add flows.yaml hash to the hash store
337
+ const flowsYamlFilePath = flowsYamlPath(customer.idn);
338
+ newHashes[flowsYamlFilePath] = sha256(flowsYamlContent);
339
+
340
+ // Save hashes (now including flows.yaml and attributes.yaml)
341
+ await saveHashes(newHashes, customer.idn);
342
+ }
343
+
344
+ /**
345
+ * Pull all projects for a customer
346
+ */
347
+ export async function pullAll(
348
+ client: AxiosInstance,
349
+ customer: CustomerConfig,
350
+ projectId: string | null = null,
351
+ verbose: boolean = false,
352
+ silentOverwrite: boolean = false
353
+ ): Promise<void> {
354
+ if (verbose) console.log(`🔄 Starting pull operation for customer ${customer.idn}...`);
355
+
356
+ await pullSingleProject(client, customer, projectId, verbose, silentOverwrite);
357
+
358
+ if (verbose) console.log(`✅ Pull completed for customer ${customer.idn}`);
359
+ }
@@ -0,0 +1,200 @@
1
+ /**
2
+ * Push operations for changed files
3
+ */
4
+ import { updateSkill } from '../api.js';
5
+ import {
6
+ ensureState,
7
+ mapPath,
8
+ skillMetadataPath
9
+ } from '../fsutil.js';
10
+ import {
11
+ validateSkillFolder,
12
+ getSingleSkillFile
13
+ } from './skill-files.js';
14
+ import fs from 'fs-extra';
15
+ import { sha256, loadHashes, saveHashes } from '../hash.js';
16
+ import yaml from 'js-yaml';
17
+ import { generateFlowsYaml } from './metadata.js';
18
+ import { isProjectMap, isLegacyProjectMap } from './projects.js';
19
+ import { flowsYamlPath } from '../fsutil.js';
20
+ import type { AxiosInstance } from 'axios';
21
+ import type {
22
+ ProjectData,
23
+ ProjectMap,
24
+ CustomerConfig,
25
+ SkillMetadata
26
+ } from '../types.js';
27
+
28
+ /**
29
+ * Push changed files to NEWO platform
30
+ */
31
+ export async function pushChanged(client: AxiosInstance, customer: CustomerConfig, verbose: boolean = false): Promise<void> {
32
+ await ensureState(customer.idn);
33
+ if (!(await fs.pathExists(mapPath(customer.idn)))) {
34
+ console.log(`No map for customer ${customer.idn}. Run \`newo pull --customer ${customer.idn}\` first.`);
35
+ return;
36
+ }
37
+
38
+ if (verbose) console.log(`📋 Loading project mapping and hashes for customer ${customer.idn}...`);
39
+ const idMapData = await fs.readJson(mapPath(customer.idn)) as unknown;
40
+ const hashes = await loadHashes(customer.idn);
41
+ const newHashes = { ...hashes };
42
+ let pushed = 0;
43
+ let scanned = 0;
44
+ let metadataChanged = false;
45
+
46
+ // Handle both old single-project format and new multi-project format with type guards
47
+ const projects = isProjectMap(idMapData) && idMapData.projects
48
+ ? idMapData.projects
49
+ : isLegacyProjectMap(idMapData)
50
+ ? { '': idMapData as ProjectData }
51
+ : (() => { throw new Error('Invalid project map format'); })();
52
+
53
+ for (const [projectIdn, projectData] of Object.entries(projects)) {
54
+ if (verbose && projectIdn) console.log(`📁 Checking project: ${projectIdn}`);
55
+
56
+ for (const [agentIdn, agentObj] of Object.entries(projectData.agents)) {
57
+ if (verbose) console.log(` 📁 Checking agent: ${agentIdn}`);
58
+ for (const [flowIdn, flowObj] of Object.entries(agentObj.flows)) {
59
+ if (verbose) console.log(` 📁 Checking flow: ${flowIdn}`);
60
+ for (const [skillIdn, skillMeta] of Object.entries(flowObj.skills)) {
61
+ scanned++;
62
+
63
+ // Validate skill folder has exactly one script file
64
+ const validation = await validateSkillFolder(customer.idn, projectIdn, agentIdn, flowIdn, skillIdn);
65
+
66
+ if (!validation.isValid) {
67
+ // Show warnings and errors
68
+ validation.errors.forEach(error => {
69
+ console.error(`❌ ${error}`);
70
+ });
71
+ validation.warnings.forEach(warning => {
72
+ console.warn(`⚠️ ${warning}`);
73
+ });
74
+
75
+ if (validation.files.length > 1) {
76
+ console.warn(`⚠️ Skipping push for skill ${skillIdn} - multiple script files found:`);
77
+ validation.files.forEach(file => {
78
+ console.warn(` • ${file.fileName}`);
79
+ });
80
+ console.warn(` Please keep only one script file and try again.`);
81
+ }
82
+ continue;
83
+ }
84
+
85
+ // Get the single valid script file
86
+ const skillFile = await getSingleSkillFile(customer.idn, projectIdn, agentIdn, flowIdn, skillIdn);
87
+ if (!skillFile) {
88
+ if (verbose) console.log(` ❌ No valid script file found for: ${skillIdn}`);
89
+ continue;
90
+ }
91
+
92
+ const content = skillFile.content;
93
+ const currentPath = skillFile.filePath;
94
+ const h = sha256(content);
95
+ const oldHash = hashes[currentPath];
96
+
97
+ if (oldHash !== h) {
98
+ if (verbose) console.log(`🔄 Script changed, updating: ${skillIdn} (${skillFile.fileName})`);
99
+
100
+ try {
101
+ // Create skill object for update
102
+ const skillObject = {
103
+ id: skillMeta.id,
104
+ title: skillMeta.title,
105
+ idn: skillMeta.idn,
106
+ prompt_script: content,
107
+ runner_type: skillMeta.runner_type,
108
+ model: skillMeta.model,
109
+ parameters: skillMeta.parameters,
110
+ path: skillMeta.path || undefined
111
+ };
112
+
113
+ await updateSkill(client, skillObject);
114
+ console.log(`↑ Pushed: ${skillIdn} (${skillMeta.title}) from ${skillFile.fileName}`);
115
+
116
+ newHashes[currentPath] = h;
117
+ pushed++;
118
+ } catch (error) {
119
+ console.error(`❌ Failed to push ${skillIdn}: ${error instanceof Error ? error.message : String(error)}`);
120
+ }
121
+ } else if (verbose) {
122
+ console.log(` ✓ No changes: ${skillIdn} (${skillFile.fileName})`);
123
+ }
124
+ }
125
+
126
+ // Check for metadata-only changes and push them separately
127
+ for (const [skillIdn] of Object.entries(flowObj.skills)) {
128
+ const metadataPath = projectIdn ?
129
+ skillMetadataPath(customer.idn, projectIdn, agentIdn, flowIdn, skillIdn) :
130
+ skillMetadataPath(customer.idn, '', agentIdn, flowIdn, skillIdn);
131
+
132
+ if (await fs.pathExists(metadataPath)) {
133
+ const metadataContent = await fs.readFile(metadataPath, 'utf8');
134
+ const h = sha256(metadataContent);
135
+ const oldHash = hashes[metadataPath];
136
+
137
+ if (oldHash !== h) {
138
+ if (verbose) console.log(`🔄 Metadata-only change detected for ${skillIdn}, updating skill...`);
139
+
140
+ try {
141
+ // Load updated metadata
142
+ const updatedMetadata = yaml.load(metadataContent) as SkillMetadata;
143
+
144
+ // Get current script content using file validation
145
+ const skillFile = await getSingleSkillFile(customer.idn, projectIdn, agentIdn, flowIdn, skillIdn);
146
+ let scriptContent = '';
147
+
148
+ if (skillFile) {
149
+ scriptContent = skillFile.content;
150
+ } else {
151
+ console.warn(`⚠️ No valid script file found for metadata update: ${skillIdn}`);
152
+ continue;
153
+ }
154
+
155
+ // Create skill object with updated metadata
156
+ const skillObject = {
157
+ id: updatedMetadata.id,
158
+ title: updatedMetadata.title,
159
+ idn: updatedMetadata.idn,
160
+ prompt_script: scriptContent,
161
+ runner_type: updatedMetadata.runner_type,
162
+ model: updatedMetadata.model,
163
+ parameters: updatedMetadata.parameters,
164
+ path: updatedMetadata.path || undefined
165
+ };
166
+
167
+ await updateSkill(client, skillObject);
168
+ console.log(`↑ Pushed metadata update for skill: ${skillIdn} (${updatedMetadata.title})`);
169
+
170
+ newHashes[metadataPath] = h;
171
+ pushed++;
172
+ metadataChanged = true;
173
+
174
+ } catch (error) {
175
+ console.error(`❌ Failed to push metadata for ${skillIdn}: ${error instanceof Error ? error.message : String(error)}`);
176
+ }
177
+ }
178
+ }
179
+ }
180
+ }
181
+ }
182
+ }
183
+
184
+ if (verbose) console.log(`🔄 Scanned ${scanned} files, found ${pushed} changes`);
185
+
186
+ // Regenerate flows.yaml if metadata was changed
187
+ if (metadataChanged) {
188
+ if (verbose) console.log(`🔄 Regenerating flows.yaml due to metadata changes...`);
189
+ const flowsYamlContent = await generateFlowsYaml({ projects } as ProjectMap, customer.idn, verbose);
190
+
191
+ // Update hash for flows.yaml
192
+ const flowsYamlFilePath = flowsYamlPath(customer.idn);
193
+ newHashes[flowsYamlFilePath] = sha256(flowsYamlContent);
194
+ }
195
+
196
+ // Save updated hashes
197
+ await saveHashes(newHashes, customer.idn);
198
+
199
+ console.log(pushed ? `${pushed} file(s) pushed.` : 'No changes to push.');
200
+ }