newo 1.9.2 → 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 +116 -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 -1226
  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 -1418
  66. package/src/types.ts +0 -1
@@ -0,0 +1,110 @@
1
+ /**
2
+ * Customer attributes synchronization module
3
+ */
4
+ import { getCustomerAttributes } from '../api.js';
5
+ import {
6
+ writeFileSafe,
7
+ customerAttributesPath,
8
+ customerAttributesMapPath,
9
+ customerAttributesBackupPath
10
+ } from '../fsutil.js';
11
+ import yaml from 'js-yaml';
12
+ import type { AxiosInstance } from 'axios';
13
+ import type { CustomerConfig } from '../types.js';
14
+
15
+ /**
16
+ * Save customer attributes to YAML format and return content for hashing
17
+ */
18
+ export async function saveCustomerAttributes(
19
+ client: AxiosInstance,
20
+ customer: CustomerConfig,
21
+ verbose: boolean = false
22
+ ): Promise<string> {
23
+ if (verbose) console.log(`🔍 Fetching customer attributes for ${customer.idn}...`);
24
+
25
+ try {
26
+ const response = await getCustomerAttributes(client, true); // Include hidden attributes
27
+
28
+ // API returns { groups: [...], attributes: [...] }
29
+ // We only want the attributes array in the expected format
30
+ const attributes = response.attributes || response;
31
+ if (verbose) console.log(`📦 Found ${Array.isArray(attributes) ? attributes.length : 'invalid'} attributes`);
32
+
33
+ // Create ID mapping for push operations (separate from YAML)
34
+ const idMapping: Record<string, string> = {};
35
+
36
+ // Transform attributes to match reference format exactly (no ID fields)
37
+ const cleanAttributes = Array.isArray(attributes) ? attributes.map(attr => {
38
+ // Store ID mapping for push operations
39
+ if (attr.id) {
40
+ idMapping[attr.idn] = attr.id;
41
+ }
42
+
43
+ // Special handling for complex JSON string values
44
+ let processedValue = attr.value;
45
+ if (typeof attr.value === 'string' && attr.value.startsWith('[{') && attr.value.endsWith('}]')) {
46
+ try {
47
+ // Parse and reformat JSON for better readability
48
+ const parsed = JSON.parse(attr.value);
49
+ processedValue = JSON.stringify(parsed, null, 0); // No extra spacing, but valid JSON
50
+ } catch (e) {
51
+ // Keep original if parsing fails
52
+ processedValue = attr.value;
53
+ }
54
+ }
55
+
56
+ const cleanAttr: any = {
57
+ idn: attr.idn,
58
+ value: processedValue,
59
+ title: attr.title || "",
60
+ description: attr.description || "",
61
+ group: attr.group || "",
62
+ is_hidden: attr.is_hidden,
63
+ possible_values: attr.possible_values || [],
64
+ value_type: `__ENUM_PLACEHOLDER_${attr.value_type}__`
65
+ };
66
+ return cleanAttr;
67
+ }) : [];
68
+
69
+ const attributesYaml = {
70
+ attributes: cleanAttributes
71
+ };
72
+
73
+ // Configure YAML output to match reference format exactly
74
+ let yamlContent = yaml.dump(attributesYaml, {
75
+ indent: 2,
76
+ quotingType: '"',
77
+ forceQuotes: false,
78
+ lineWidth: 80, // Wrap long lines to match reference format
79
+ noRefs: true,
80
+ sortKeys: false,
81
+ flowLevel: -1, // Never use flow syntax
82
+ styles: {
83
+ '!!str': 'folded' // Use folded style for better line wrapping of long strings
84
+ }
85
+ });
86
+
87
+ // Post-process to fix enum format and improve JSON string formatting
88
+ yamlContent = yamlContent.replace(/__ENUM_PLACEHOLDER_(\w+)__/g, '!enum "AttributeValueTypes.$1"');
89
+
90
+ // Fix JSON string formatting to match reference (remove escape characters)
91
+ yamlContent = yamlContent.replace(/\\"/g, '"');
92
+
93
+ // Save all files: attributes.yaml, ID mapping, and backup for diff tracking
94
+ await writeFileSafe(customerAttributesPath(customer.idn), yamlContent);
95
+ await writeFileSafe(customerAttributesMapPath(customer.idn), JSON.stringify(idMapping, null, 2));
96
+ await writeFileSafe(customerAttributesBackupPath(customer.idn), yamlContent);
97
+
98
+ if (verbose) {
99
+ console.log(`✓ Saved customer attributes to ${customerAttributesPath(customer.idn)}`);
100
+ console.log(`✓ Saved attribute ID mapping to ${customerAttributesMapPath(customer.idn)}`);
101
+ console.log(`✓ Created attributes backup for diff tracking`);
102
+ }
103
+
104
+ // Return content for hash calculation
105
+ return yamlContent;
106
+ } catch (error) {
107
+ console.error(`❌ Failed to save customer attributes for ${customer.idn}:`, error);
108
+ throw error;
109
+ }
110
+ }
@@ -0,0 +1,257 @@
1
+ /**
2
+ * Conversations synchronization module
3
+ */
4
+ import { listUserPersonas, getChatHistory } from '../api.js';
5
+ import { writeFileSafe } from '../fsutil.js';
6
+ import yaml from 'js-yaml';
7
+ import pLimit from 'p-limit';
8
+ import type { AxiosInstance } from 'axios';
9
+ import type {
10
+ CustomerConfig,
11
+ ConversationOptions,
12
+ UserPersona,
13
+ ConversationAct,
14
+ ProcessedPersona,
15
+ ProcessedAct,
16
+ ConversationsData
17
+ } from '../types.js';
18
+
19
+ // Concurrency limit for API calls
20
+ const concurrencyLimit = pLimit(5);
21
+
22
+ /**
23
+ * Pull conversations for a customer and save to YAML
24
+ */
25
+ export async function pullConversations(
26
+ client: AxiosInstance,
27
+ customer: CustomerConfig,
28
+ options: ConversationOptions = {},
29
+ verbose: boolean = false
30
+ ): Promise<void> {
31
+ if (verbose) console.log(`💬 Fetching conversations for customer ${customer.idn}...`);
32
+
33
+ try {
34
+ // Get all user personas with pagination
35
+ const allPersonas: UserPersona[] = [];
36
+ let page = 1;
37
+ const perPage = 50;
38
+ let hasMore = true;
39
+
40
+ while (hasMore) {
41
+ const response = await listUserPersonas(client, page, perPage);
42
+ allPersonas.push(...response.items);
43
+
44
+ if (verbose) console.log(`📋 Page ${page}: Found ${response.items.length} personas (${allPersonas.length}/${response.metadata.total} total)`);
45
+
46
+ hasMore = response.items.length === perPage && allPersonas.length < response.metadata.total;
47
+ page++;
48
+ }
49
+
50
+ if (options.maxPersonas && allPersonas.length > options.maxPersonas) {
51
+ allPersonas.splice(options.maxPersonas);
52
+ if (verbose) console.log(`⚠️ Limited to ${options.maxPersonas} personas as requested`);
53
+ }
54
+
55
+ if (verbose) console.log(`👥 Processing ${allPersonas.length} personas...`);
56
+
57
+ // Process personas concurrently with limited concurrency
58
+ const processedPersonas: ProcessedPersona[] = [];
59
+
60
+ await Promise.all(allPersonas.map(persona => concurrencyLimit(async () => {
61
+ try {
62
+ // Extract phone number from actors
63
+ const phoneActor = persona.actors.find(actor =>
64
+ actor.integration_idn === 'newo_voice' &&
65
+ actor.connector_idn === 'newo_voice_connector' &&
66
+ actor.contact_information?.startsWith('+')
67
+ );
68
+ const phone = phoneActor?.contact_information || null;
69
+
70
+ // Get acts for this persona
71
+ const allActs: ConversationAct[] = [];
72
+ let actPage = 1;
73
+ const actsPerPage = 100; // Higher limit for acts
74
+ let hasMoreActs = true;
75
+
76
+ // Get user actor IDs from persona actors first
77
+ const userActors = persona.actors.filter(actor =>
78
+ actor.integration_idn === 'newo_voice' &&
79
+ actor.connector_idn === 'newo_voice_connector'
80
+ );
81
+
82
+ if (userActors.length === 0) {
83
+ if (verbose) console.log(` 👤 ${persona.name}: No voice actors found, skipping`);
84
+ // No voice actors, can't get chat history - add persona with empty acts
85
+ processedPersonas.push({
86
+ id: persona.id,
87
+ name: persona.name,
88
+ phone,
89
+ act_count: persona.act_count,
90
+ acts: []
91
+ });
92
+ if (verbose) console.log(` ✓ Processed ${persona.name}: 0 acts (no voice actors)`);
93
+ return; // Return from the concurrency function
94
+ }
95
+
96
+ // Safety mechanism to prevent infinite loops
97
+ const maxPages = 50; // Limit to 50 pages (5000 acts max per persona)
98
+
99
+ while (hasMoreActs && actPage <= maxPages) {
100
+ try {
101
+ const chatHistoryParams = {
102
+ user_actor_id: userActors[0]!.id,
103
+ page: actPage,
104
+ per: actsPerPage
105
+ };
106
+
107
+ if (verbose) console.log(` 📄 ${persona.name}: Fetching page ${actPage}...`);
108
+ const chatResponse = await getChatHistory(client, chatHistoryParams);
109
+
110
+ if (chatResponse.items && chatResponse.items.length > 0) {
111
+ // Convert chat history format to acts format - create minimal ConversationAct objects
112
+ const convertedActs: ConversationAct[] = chatResponse.items.map((item: any) => ({
113
+ id: item.id || `chat_${Math.random()}`,
114
+ command_act_id: null,
115
+ external_event_id: item.external_event_id || 'chat_history',
116
+ arguments: [],
117
+ reference_idn: (item.is_agent === true) ? 'agent_message' : 'user_message',
118
+ runtime_context_id: item.runtime_context_id || 'chat_history',
119
+ source_text: item.payload?.text || item.message || item.content || item.text || '',
120
+ original_text: item.payload?.text || item.message || item.content || item.text || '',
121
+ datetime: item.datetime || item.created_at || item.timestamp || new Date().toISOString(),
122
+ user_actor_id: userActors[0]!.id,
123
+ agent_actor_id: null,
124
+ user_persona_id: persona.id,
125
+ user_persona_name: persona.name,
126
+ agent_persona_id: item.agent_persona_id || 'unknown',
127
+ external_id: item.external_id || null,
128
+ integration_idn: 'newo_voice',
129
+ connector_idn: 'newo_voice_connector',
130
+ to_integration_idn: null,
131
+ to_connector_idn: null,
132
+ is_agent: Boolean(item.is_agent === true),
133
+ project_idn: null,
134
+ flow_idn: item.flow_idn || 'unknown',
135
+ skill_idn: item.skill_idn || 'unknown',
136
+ session_id: item.session_id || 'unknown',
137
+ recordings: item.recordings || [],
138
+ contact_information: item.contact_information || null
139
+ }));
140
+
141
+ allActs.push(...convertedActs);
142
+
143
+ if (verbose && convertedActs.length > 0) {
144
+ console.log(` 👤 ${persona.name}: Chat History - ${convertedActs.length} messages (${allActs.length} total)`);
145
+ }
146
+
147
+ // Check if we should continue paginating
148
+ const hasMetadata = chatResponse.metadata?.total !== undefined;
149
+ const currentTotal = chatResponse.metadata?.total || 0;
150
+
151
+ hasMoreActs = chatResponse.items.length === actsPerPage &&
152
+ hasMetadata &&
153
+ allActs.length < currentTotal;
154
+
155
+ actPage++;
156
+
157
+ if (verbose) console.log(` 📊 ${persona.name}: Page ${actPage - 1} done, ${allActs.length}/${currentTotal} total acts`);
158
+ } else {
159
+ // No more items
160
+ hasMoreActs = false;
161
+ if (verbose) console.log(` 📊 ${persona.name}: No more chat history items`);
162
+ }
163
+ } catch (chatError) {
164
+ if (verbose) console.log(` ⚠️ Chat history failed for ${persona.name}: ${chatError instanceof Error ? chatError.message : String(chatError)}`);
165
+ hasMoreActs = false;
166
+ }
167
+ }
168
+
169
+ if (actPage > maxPages) {
170
+ if (verbose) console.log(` ⚠️ ${persona.name}: Reached max pages limit (${maxPages}), stopping pagination`);
171
+ }
172
+
173
+ // Sort acts by datetime ascending (chronological order)
174
+ allActs.sort((a, b) => new Date(a.datetime).getTime() - new Date(b.datetime).getTime());
175
+
176
+ // Process acts into simplified format - exclude redundant fields
177
+ const processedActs: ProcessedAct[] = allActs.map(act => {
178
+ const processedAct: ProcessedAct = {
179
+ datetime: act.datetime,
180
+ type: act.reference_idn,
181
+ message: act.source_text
182
+ };
183
+
184
+ // Only include non-redundant fields
185
+ if (act.contact_information) {
186
+ (processedAct as any).contact_information = act.contact_information;
187
+ }
188
+ if (act.flow_idn && act.flow_idn !== 'unknown') {
189
+ (processedAct as any).flow_idn = act.flow_idn;
190
+ }
191
+ if (act.skill_idn && act.skill_idn !== 'unknown') {
192
+ (processedAct as any).skill_idn = act.skill_idn;
193
+ }
194
+ if (act.session_id && act.session_id !== 'unknown') {
195
+ (processedAct as any).session_id = act.session_id;
196
+ }
197
+
198
+ return processedAct;
199
+ });
200
+
201
+ processedPersonas.push({
202
+ id: persona.id,
203
+ name: persona.name,
204
+ phone,
205
+ act_count: persona.act_count,
206
+ acts: processedActs
207
+ });
208
+
209
+ if (verbose) console.log(` ✓ Processed ${persona.name}: ${processedActs.length} acts`);
210
+ } catch (error) {
211
+ console.error(`❌ Failed to process persona ${persona.name}:`, error);
212
+ // Continue with other personas
213
+ }
214
+ })));
215
+
216
+ // Sort personas by most recent act time (descending) - use latest act from acts array
217
+ processedPersonas.sort((a, b) => {
218
+ const aLatestTime = a.acts.length > 0 ? a.acts[a.acts.length - 1]!.datetime : '1970-01-01T00:00:00.000Z';
219
+ const bLatestTime = b.acts.length > 0 ? b.acts[b.acts.length - 1]!.datetime : '1970-01-01T00:00:00.000Z';
220
+ return new Date(bLatestTime).getTime() - new Date(aLatestTime).getTime();
221
+ });
222
+
223
+ // Calculate totals
224
+ const totalActs = processedPersonas.reduce((sum, persona) => sum + persona.acts.length, 0);
225
+
226
+ // Create final conversations data
227
+ const conversationsData: ConversationsData = {
228
+ personas: processedPersonas,
229
+ total_personas: processedPersonas.length,
230
+ total_acts: totalActs,
231
+ generated_at: new Date().toISOString()
232
+ };
233
+
234
+ // Save to YAML file
235
+ const conversationsPath = `newo_customers/${customer.idn}/conversations.yaml`;
236
+ const yamlContent = yaml.dump(conversationsData, {
237
+ indent: 2,
238
+ quotingType: '"',
239
+ forceQuotes: false,
240
+ lineWidth: 120,
241
+ noRefs: true,
242
+ sortKeys: false,
243
+ flowLevel: -1
244
+ });
245
+
246
+ await writeFileSafe(conversationsPath, yamlContent);
247
+
248
+ if (verbose) {
249
+ console.log(`✓ Saved conversations to ${conversationsPath}`);
250
+ console.log(`📊 Summary: ${processedPersonas.length} personas, ${totalActs} total acts`);
251
+ }
252
+
253
+ } catch (error) {
254
+ console.error(`❌ Failed to pull conversations for ${customer.idn}:`, error);
255
+ throw error;
256
+ }
257
+ }
@@ -0,0 +1,153 @@
1
+ /**
2
+ * Metadata and flows.yaml generation operations
3
+ */
4
+ import {
5
+ writeFileSafe,
6
+ flowMetadataPath,
7
+ agentMetadataPath,
8
+ flowsYamlPath
9
+ } from '../fsutil.js';
10
+ import fs from 'fs-extra';
11
+ import yaml from 'js-yaml';
12
+ import type {
13
+ ProjectData,
14
+ ProjectMap,
15
+ FlowsYamlData,
16
+ FlowsYamlFlow,
17
+ FlowsYamlSkill,
18
+ FlowMetadata,
19
+ AgentMetadata,
20
+ SkillMetadata
21
+ } from '../types.js';
22
+
23
+ /**
24
+ * Generate flows.yaml file from project data
25
+ */
26
+ export async function generateFlowsYaml(
27
+ projectMap: ProjectMap | { [key: string]: ProjectData },
28
+ customerIdn: string,
29
+ verbose: boolean = false
30
+ ): Promise<string> {
31
+ if (verbose) console.log(`📊 Generating flows.yaml for customer ${customerIdn}...`);
32
+
33
+ const flowsData: FlowsYamlData = {
34
+ flows: []
35
+ };
36
+
37
+ // Handle both formats
38
+ const projects = 'projects' in projectMap ? projectMap.projects : projectMap;
39
+
40
+ for (const [projectIdn, projectData] of Object.entries(projects)) {
41
+ if (verbose && projectIdn) console.log(` 📁 Processing project: ${projectIdn}`);
42
+
43
+ for (const [agentIdn, agentData] of Object.entries(projectData.agents as Record<string, any>)) {
44
+ if (verbose) console.log(` 📁 Processing agent: ${agentIdn}`);
45
+
46
+ const agentFlows: FlowsYamlFlow[] = [];
47
+
48
+ for (const [flowIdn, flowData] of Object.entries(agentData.flows as Record<string, any>)) {
49
+ if (verbose) console.log(` 📁 Processing flow: ${flowIdn}`);
50
+
51
+ // Load flow metadata to get comprehensive flow information
52
+ const flowMetaPath = flowMetadataPath(customerIdn, projectIdn, agentIdn, flowIdn);
53
+ let flowMeta: FlowMetadata | null = null;
54
+
55
+ try {
56
+ if (await fs.pathExists(flowMetaPath)) {
57
+ const flowMetaContent = await fs.readFile(flowMetaPath, 'utf8');
58
+ flowMeta = yaml.load(flowMetaContent) as FlowMetadata;
59
+ }
60
+ } catch (e) {
61
+ if (verbose) console.log(` ⚠️ Could not load flow metadata: ${flowMetaPath}`);
62
+ }
63
+
64
+ const skills: FlowsYamlSkill[] = [];
65
+ for (const [, skillMeta] of Object.entries(flowData.skills as Record<string, SkillMetadata>)) {
66
+ // Note: We don't need to load script content since prompt_script is excluded from flows.yaml
67
+
68
+ skills.push({
69
+ idn: skillMeta.idn,
70
+ title: skillMeta.title,
71
+ runner_type: skillMeta.runner_type,
72
+ model: skillMeta.model,
73
+ parameters: skillMeta.parameters.map((p: any) => ({
74
+ name: p.name,
75
+ default_value: p.default_value || ''
76
+ }))
77
+ });
78
+ }
79
+
80
+ // Use flow metadata if available, otherwise use basic info
81
+ const flowYaml: FlowsYamlFlow = {
82
+ idn: flowIdn,
83
+ title: flowMeta?.title || 'Unknown Flow',
84
+ description: flowMeta?.description || null,
85
+ default_runner_type: flowMeta?.default_runner_type || 'guidance',
86
+ default_provider_idn: flowMeta?.default_model?.provider_idn || 'openai',
87
+ default_model_idn: flowMeta?.default_model?.model_idn || 'gpt-4',
88
+ skills,
89
+ events: flowMeta?.events?.map(event => ({
90
+ title: event.description,
91
+ idn: event.idn,
92
+ skill_selector: event.skill_selector,
93
+ skill_idn: event.skill_idn || null,
94
+ state_idn: event.state_idn || null,
95
+ integration_idn: event.integration_idn || null,
96
+ connector_idn: event.connector_idn || null,
97
+ interrupt_mode: event.interrupt_mode
98
+ })) || [],
99
+ state_fields: flowMeta?.state_fields?.map(state => ({
100
+ title: state.title,
101
+ idn: state.idn,
102
+ default_value: state.default_value || null,
103
+ scope: state.scope
104
+ })) || []
105
+ };
106
+
107
+ agentFlows.push(flowYaml);
108
+ }
109
+
110
+ if (agentFlows.length > 0) {
111
+ // Load agent metadata for description
112
+ const agentMetaPath = agentMetadataPath(customerIdn, projectIdn, agentIdn);
113
+ let agentDescription: string | null = null;
114
+
115
+ try {
116
+ if (await fs.pathExists(agentMetaPath)) {
117
+ const agentMetaContent = await fs.readFile(agentMetaPath, 'utf8');
118
+ const agentMeta = yaml.load(agentMetaContent) as AgentMetadata;
119
+ agentDescription = agentMeta.description || null;
120
+ }
121
+ } catch (e) {
122
+ if (verbose) console.log(` ⚠️ Could not load agent metadata: ${agentMetaPath}`);
123
+ }
124
+
125
+ flowsData.flows.push({
126
+ agent_idn: agentIdn,
127
+ agent_description: agentDescription,
128
+ agent_flows: agentFlows
129
+ });
130
+ }
131
+ }
132
+ }
133
+
134
+ // Generate flows.yaml content
135
+ const flowsYamlContent = yaml.dump(flowsData, {
136
+ indent: 2,
137
+ quotingType: '"',
138
+ forceQuotes: false,
139
+ lineWidth: 120,
140
+ noRefs: true,
141
+ sortKeys: false,
142
+ flowLevel: -1
143
+ });
144
+
145
+ // Save flows.yaml
146
+ const flowsFilePath = flowsYamlPath(customerIdn);
147
+ await writeFileSafe(flowsFilePath, flowsYamlContent);
148
+
149
+ if (verbose) console.log(`✓ Generated flows.yaml with ${flowsData.flows.length} agents`);
150
+
151
+ // Return content for hash calculation
152
+ return flowsYamlContent;
153
+ }