newo 3.2.0 → 3.3.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.
@@ -0,0 +1,746 @@
1
+ /**
2
+ * Account migration operations
3
+ * Migrates complete account from source to destination
4
+ */
5
+
6
+ import {
7
+ listProjects, listAgents, createProject, createAgent, createFlow, createSkill,
8
+ createFlowEvent, createFlowState,
9
+ getCustomerAttributes, createCustomerAttribute, updateCustomerAttribute,
10
+ getProjectAttributes, createProjectAttribute, updateProjectAttribute,
11
+ searchPersonas, getAkbTopics, createPersona, importAkbArticle,
12
+ listIntegrations, listConnectors, createConnector,
13
+ listFlowSkills, updateSkill
14
+ } from '../api.js';
15
+ import { pullAll } from './projects.js';
16
+ import { pullIntegrations } from './integrations.js';
17
+ import { customerDir, customerProjectsDir } from '../fsutil.js';
18
+ import fs from 'fs-extra';
19
+ import yaml from 'js-yaml';
20
+ import path from 'path';
21
+ import type { AxiosInstance } from 'axios';
22
+ import type {
23
+ CustomerConfig,
24
+ CreateFlowEventRequest, AkbImportArticle
25
+ } from '../types.js';
26
+
27
+ interface MigrationOptions {
28
+ sourceCustomer: CustomerConfig;
29
+ destCustomer: CustomerConfig;
30
+ sourceClient: AxiosInstance;
31
+ destClient: AxiosInstance;
32
+ verbose: boolean;
33
+ }
34
+
35
+ interface MigrationResult {
36
+ success: boolean;
37
+ projectsCreated: number;
38
+ agentsCreated: number;
39
+ flowsCreated: number;
40
+ skillsCreated: number;
41
+ attributesMigrated: number;
42
+ personasCreated: number;
43
+ articlesImported: number;
44
+ connectorsCreated: number;
45
+ webhooksCreated: number;
46
+ errors: string[];
47
+ }
48
+
49
+ /**
50
+ * Migrate complete account from source to destination
51
+ */
52
+ export async function migrateAccount(options: MigrationOptions): Promise<MigrationResult> {
53
+ const { sourceCustomer, destCustomer, sourceClient, destClient, verbose } = options;
54
+
55
+ const result: MigrationResult = {
56
+ success: false,
57
+ projectsCreated: 0,
58
+ agentsCreated: 0,
59
+ flowsCreated: 0,
60
+ skillsCreated: 0,
61
+ attributesMigrated: 0,
62
+ personasCreated: 0,
63
+ articlesImported: 0,
64
+ connectorsCreated: 0,
65
+ webhooksCreated: 0,
66
+ errors: []
67
+ };
68
+
69
+ try {
70
+ console.log('\nšŸ”„ Starting account migration...');
71
+ console.log(`Source: ${sourceCustomer.idn}`);
72
+ console.log(`Destination: ${destCustomer.idn}\n`);
73
+
74
+ // Step 1: Pull source data
75
+ console.log('šŸ“„ Step 1: Pulling source data...');
76
+ await pullAll(sourceClient, sourceCustomer, null, verbose, true);
77
+ await pullIntegrations(sourceClient, customerDir(sourceCustomer.idn), verbose);
78
+ console.log(' āœ… Source data pulled\n');
79
+
80
+ // Step 2: Create project structure
81
+ console.log('šŸ—ļø Step 2: Creating project structure...');
82
+ const projectCounts = await migrateProjectStructure(sourceClient, destClient, sourceCustomer, destCustomer, verbose);
83
+ result.projectsCreated = projectCounts.projects;
84
+ result.agentsCreated = projectCounts.agents;
85
+ result.flowsCreated = projectCounts.flows;
86
+ result.skillsCreated = projectCounts.skills;
87
+ console.log(` āœ… Created: ${result.projectsCreated} projects, ${result.agentsCreated} agents, ${result.flowsCreated} flows, ${result.skillsCreated} skills\n`);
88
+
89
+ // Step 3: Migrate attributes
90
+ console.log('šŸ“Š Step 3: Migrating attributes...');
91
+ result.attributesMigrated = await migrateAttributes(sourceClient, destClient, sourceCustomer, destCustomer, verbose);
92
+ console.log(` āœ… Migrated: ${result.attributesMigrated} attributes\n`);
93
+
94
+ // Step 4: Migrate AKB
95
+ console.log('šŸ“š Step 4: Migrating AKB...');
96
+ const akbCounts = await migrateAKB(sourceClient, destClient, verbose);
97
+ result.personasCreated = akbCounts.personas;
98
+ result.articlesImported = akbCounts.articles;
99
+ console.log(` āœ… Migrated: ${result.personasCreated} personas, ${result.articlesImported} articles\n`);
100
+
101
+ // Step 5: Migrate integrations
102
+ console.log('šŸ”Œ Step 5: Migrating integrations...');
103
+ result.connectorsCreated = await migrateIntegrationConnectors(sourceClient, destClient, verbose);
104
+ console.log(` āœ… Created: ${result.connectorsCreated} connectors\n`);
105
+
106
+ // Step 6: Copy files
107
+ console.log('šŸ“ Step 6: Copying files...');
108
+ await copyAccountFiles(sourceCustomer.idn, destCustomer.idn);
109
+ console.log(' āœ… Files copied\n');
110
+
111
+ // Step 7: Build map from API
112
+ console.log('šŸ“ Step 7: Building destination mappings...');
113
+ await buildMapFromAPI(destClient, destCustomer, verbose);
114
+ console.log(' āœ… Mappings built\n');
115
+
116
+ // Step 8: Push skill content
117
+ console.log('šŸ“¤ Step 8: Pushing skill content...');
118
+ const skillsPushed = await pushSkillContent(destClient, destCustomer, verbose);
119
+ console.log(` āœ… Pushed: ${skillsPushed} skills\n`);
120
+
121
+ // Step 9: Create webhooks
122
+ console.log('šŸ“” Step 9: Creating webhooks...');
123
+ result.webhooksCreated = await createWebhooksFromYAML(destClient, destCustomer, verbose);
124
+ console.log(` āœ… Created: ${result.webhooksCreated} webhooks\n`);
125
+
126
+ // Step 10: Verify
127
+ console.log('āœ… Step 10: Verifying migration...');
128
+ await verifyMigration(sourceClient, destClient, sourceCustomer, destCustomer);
129
+
130
+ result.success = true;
131
+ console.log('\nšŸŽ‰ MIGRATION COMPLETED SUCCESSFULLY!\n');
132
+
133
+ } catch (error) {
134
+ result.success = false;
135
+ result.errors.push(error instanceof Error ? error.message : String(error));
136
+ console.error(`\nāŒ Migration failed: ${error instanceof Error ? error.message : String(error)}\n`);
137
+ }
138
+
139
+ return result;
140
+ }
141
+
142
+ async function migrateProjectStructure(
143
+ sourceClient: AxiosInstance,
144
+ destClient: AxiosInstance,
145
+ sourceCustomer: CustomerConfig,
146
+ // @ts-ignore - Parameter kept for API consistency
147
+ destCustomer: CustomerConfig,
148
+ // @ts-ignore - Parameter kept for future use
149
+ verbose: boolean
150
+ ): Promise<{ projects: number; agents: number; flows: number; skills: number }> {
151
+ const sourceProjects = await listProjects(sourceClient);
152
+ const destProjects = await listProjects(destClient);
153
+ const destProjectMap = new Map(destProjects.map(p => [p.idn, p]));
154
+
155
+ let projectsCreated = 0;
156
+ let agentsCreated = 0;
157
+ let flowsCreated = 0;
158
+ let skillsCreated = 0;
159
+
160
+ for (const sourceProj of sourceProjects) {
161
+ let projectId: string;
162
+
163
+ // Create or get existing project
164
+ const existingProj = destProjectMap.get(sourceProj.idn);
165
+ if (existingProj) {
166
+ projectId = existingProj.id;
167
+ if (verbose) console.log(` āœ“ Project ${sourceProj.idn} already exists`);
168
+ } else {
169
+ const projResponse = await createProject(destClient, {
170
+ idn: sourceProj.idn,
171
+ title: sourceProj.title,
172
+ description: sourceProj.description || '',
173
+ is_auto_update_enabled: sourceProj.is_auto_update_enabled || false,
174
+ registry_idn: sourceProj.registry_idn || 'production'
175
+ });
176
+ projectId = projResponse.id;
177
+ projectsCreated++;
178
+ if (verbose) console.log(` āœ… Created project: ${sourceProj.idn}`);
179
+ }
180
+
181
+ // Create agents
182
+ const sourceAgents = await listAgents(sourceClient, sourceProj.id);
183
+ const destAgents = await listAgents(destClient, projectId);
184
+ const destAgentMap = new Map(destAgents.map(a => [a.idn, a]));
185
+
186
+ for (const sourceAgent of sourceAgents) {
187
+ let agentId: string;
188
+
189
+ const existingAgent = destAgentMap.get(sourceAgent.idn);
190
+ if (existingAgent) {
191
+ agentId = existingAgent.id;
192
+ } else {
193
+ const agentResponse = await createAgent(destClient, projectId, {
194
+ idn: sourceAgent.idn,
195
+ title: sourceAgent.title || sourceAgent.idn,
196
+ description: sourceAgent.description || null
197
+ });
198
+ agentId = agentResponse.id;
199
+ agentsCreated++;
200
+ }
201
+
202
+ // Create flows
203
+ const sourceFlows = sourceAgent.flows || [];
204
+ const destAgentData = await listAgents(destClient, projectId);
205
+ const destAgentWithFlows = destAgentData.find(a => a.id === agentId);
206
+ const destFlowMap = new Map((destAgentWithFlows?.flows || []).map(f => [f.idn, f]));
207
+
208
+ for (const sourceFlow of sourceFlows) {
209
+ let flowId: string;
210
+
211
+ const existingFlow = destFlowMap.get(sourceFlow.idn);
212
+ if (existingFlow) {
213
+ flowId = existingFlow.id;
214
+ } else {
215
+ const flowResponse = await createFlow(destClient, agentId, {
216
+ idn: sourceFlow.idn,
217
+ title: sourceFlow.title
218
+ });
219
+ flowId = flowResponse.id;
220
+ flowsCreated++;
221
+ }
222
+
223
+ // Read flow metadata for events and states
224
+ const flowMetaPath = path.join(
225
+ customerProjectsDir(sourceCustomer.idn),
226
+ sourceProj.idn,
227
+ sourceAgent.idn,
228
+ sourceFlow.idn,
229
+ 'metadata.yaml'
230
+ );
231
+
232
+ if (await fs.pathExists(flowMetaPath)) {
233
+ const flowMeta = yaml.load(await fs.readFile(flowMetaPath, 'utf8')) as any;
234
+
235
+ // Create skills
236
+ const destSkills = await listFlowSkills(destClient, flowId);
237
+ const destSkillMap = new Map(destSkills.map(s => [s.idn, s]));
238
+
239
+ for (const sourceSkill of flowMeta.skills || []) {
240
+ if (destSkillMap.has(sourceSkill.idn)) continue;
241
+
242
+ try {
243
+ await createSkill(destClient, flowId, {
244
+ idn: sourceSkill.idn,
245
+ title: sourceSkill.title,
246
+ runner_type: sourceSkill.runner_type,
247
+ model: sourceSkill.model,
248
+ prompt_script: ''
249
+ });
250
+ skillsCreated++;
251
+ } catch (error: any) {
252
+ if (verbose && error.response?.status !== 409) {
253
+ console.error(` āš ļø Failed to create skill ${sourceSkill.idn}: ${error.message}`);
254
+ }
255
+ }
256
+ }
257
+
258
+ // Create events with full metadata
259
+ for (const event of flowMeta.events || []) {
260
+ try {
261
+ const eventRequest: CreateFlowEventRequest = {
262
+ idn: event.idn,
263
+ description: event.description || event.idn,
264
+ skill_selector: event.skill_selector || 'first',
265
+ interrupt_mode: event.interrupt_mode || 'allow',
266
+ integration_idn: event.integration_idn || '',
267
+ connector_idn: event.connector_idn || ''
268
+ };
269
+
270
+ if (event.skill_idn) {
271
+ (eventRequest as any).skill_idn = event.skill_idn;
272
+ }
273
+ if (event.state_idn) {
274
+ (eventRequest as any).state_idn = event.state_idn;
275
+ }
276
+
277
+ await createFlowEvent(destClient, flowId, eventRequest);
278
+ } catch (error: any) {
279
+ if (verbose && error.response?.status !== 409 && error.response?.status !== 422) {
280
+ console.error(` āš ļø Failed to create event ${event.idn}: ${error.message}`);
281
+ }
282
+ }
283
+ }
284
+
285
+ // Create states
286
+ for (const state of flowMeta.state_fields || []) {
287
+ try {
288
+ await createFlowState(destClient, flowId, {
289
+ title: state.title || state.idn,
290
+ idn: state.idn,
291
+ scope: state.scope || 'flow'
292
+ });
293
+ } catch (error: any) {
294
+ if (verbose && error.response?.status !== 409) {
295
+ console.error(` āš ļø Failed to create state ${state.idn}: ${error.message}`);
296
+ }
297
+ }
298
+ }
299
+ }
300
+ }
301
+ }
302
+ }
303
+
304
+ return { projects: projectsCreated, agents: agentsCreated, flows: flowsCreated, skills: skillsCreated };
305
+ }
306
+
307
+ async function migrateAttributes(
308
+ sourceClient: AxiosInstance,
309
+ destClient: AxiosInstance,
310
+ // @ts-ignore - Parameter kept for API consistency
311
+ sourceCustomer: CustomerConfig,
312
+ // @ts-ignore - Parameter kept for API consistency
313
+ destCustomer: CustomerConfig,
314
+ // @ts-ignore - Parameter kept for future use
315
+ verbose: boolean
316
+ ): Promise<number> {
317
+ let count = 0;
318
+
319
+ // Customer attributes
320
+ const sourceAttrs = await getCustomerAttributes(sourceClient, true);
321
+ const destAttrs = await getCustomerAttributes(destClient, true);
322
+ const destAttrMap = new Map(destAttrs.attributes.map(a => [a.idn, a]));
323
+
324
+ for (const sourceAttr of sourceAttrs.attributes) {
325
+ const destAttr = destAttrMap.get(sourceAttr.idn);
326
+
327
+ if (!destAttr) {
328
+ await createCustomerAttribute(destClient, {
329
+ idn: sourceAttr.idn,
330
+ title: sourceAttr.title,
331
+ description: sourceAttr.description || '',
332
+ value: typeof sourceAttr.value === 'object' ? JSON.stringify(sourceAttr.value) : sourceAttr.value,
333
+ value_type: sourceAttr.value_type,
334
+ group: sourceAttr.group || '',
335
+ is_hidden: sourceAttr.is_hidden || false,
336
+ possible_values: sourceAttr.possible_values || []
337
+ });
338
+ count++;
339
+ } else if (JSON.stringify(destAttr.value) !== JSON.stringify(sourceAttr.value)) {
340
+ if (destAttr.id) {
341
+ await updateCustomerAttribute(destClient, {
342
+ ...sourceAttr,
343
+ id: destAttr.id
344
+ });
345
+ count++;
346
+ }
347
+ }
348
+ }
349
+
350
+ // Project attributes
351
+ const sourceProjects = await listProjects(sourceClient);
352
+ const destProjects = await listProjects(destClient);
353
+ const destProjectMap = new Map(destProjects.map(p => [p.idn, p]));
354
+
355
+ for (const sourceProj of sourceProjects) {
356
+ const destProj = destProjectMap.get(sourceProj.idn);
357
+ if (!destProj) continue;
358
+
359
+ const sourceProjAttrs = await getProjectAttributes(sourceClient, sourceProj.id, true);
360
+ const destProjAttrs = await getProjectAttributes(destClient, destProj.id, true);
361
+ const destProjAttrMap = new Map(destProjAttrs.attributes.map(a => [a.idn, a]));
362
+
363
+ for (const sourceAttr of sourceProjAttrs.attributes) {
364
+ const destAttr = destProjAttrMap.get(sourceAttr.idn);
365
+
366
+ if (!destAttr) {
367
+ await createProjectAttribute(destClient, destProj.id, {
368
+ idn: sourceAttr.idn,
369
+ title: sourceAttr.title,
370
+ description: sourceAttr.description || '',
371
+ value: typeof sourceAttr.value === 'object' ? JSON.stringify(sourceAttr.value) : sourceAttr.value,
372
+ value_type: sourceAttr.value_type,
373
+ group: sourceAttr.group || '',
374
+ is_hidden: sourceAttr.is_hidden || false,
375
+ possible_values: sourceAttr.possible_values || []
376
+ });
377
+ count++;
378
+ } else if (JSON.stringify(destAttr.value) !== JSON.stringify(sourceAttr.value)) {
379
+ await updateProjectAttribute(destClient, destProj.id, {
380
+ ...destAttr,
381
+ value: sourceAttr.value,
382
+ title: sourceAttr.title,
383
+ description: sourceAttr.description
384
+ });
385
+ count++;
386
+ }
387
+ }
388
+ }
389
+
390
+ return count;
391
+ }
392
+
393
+ async function migrateAKB(
394
+ sourceClient: AxiosInstance,
395
+ destClient: AxiosInstance,
396
+ // @ts-ignore - Parameter kept for future use
397
+ verbose: boolean
398
+ ): Promise<{ personas: number; articles: number }> {
399
+ const sourcePersonas = await searchPersonas(sourceClient, true);
400
+ const destPersonas = await searchPersonas(destClient, true);
401
+ const destPersonaMap = new Map(destPersonas.items.map(p => [p.name, p]));
402
+
403
+ let personasCreated = 0;
404
+ let articlesImported = 0;
405
+
406
+ for (const sourcePersona of sourcePersonas.items) {
407
+ if (destPersonaMap.has(sourcePersona.name)) continue;
408
+
409
+ const newPersona = await createPersona(destClient, {
410
+ name: sourcePersona.name,
411
+ title: sourcePersona.title,
412
+ description: sourcePersona.description || ''
413
+ });
414
+ personasCreated++;
415
+
416
+ // Import articles
417
+ let page = 1;
418
+ let hasMore = true;
419
+
420
+ while (hasMore) {
421
+ const topics = await getAkbTopics(sourceClient, sourcePersona.id, page, 50);
422
+
423
+ if (topics.items.length === 0) break;
424
+
425
+ for (const topicItem of topics.items) {
426
+ const articleData: AkbImportArticle = {
427
+ persona_id: newPersona.id,
428
+ topic_name: topicItem.topic.topic_name,
429
+ source: topicItem.topic.source || '',
430
+ topic_summary: topicItem.topic.topic_summary || '',
431
+ topic_facts: topicItem.topic.topic_facts || [],
432
+ labels: topicItem.topic.labels || [],
433
+ confidence: topicItem.topic.confidence || 1.0
434
+ };
435
+
436
+ await importAkbArticle(destClient, articleData);
437
+ articlesImported++;
438
+ }
439
+
440
+ page++;
441
+ hasMore = topics.items.length >= 50;
442
+ }
443
+ }
444
+
445
+ return { personas: personasCreated, articles: articlesImported };
446
+ }
447
+
448
+ async function migrateIntegrationConnectors(
449
+ sourceClient: AxiosInstance,
450
+ destClient: AxiosInstance,
451
+ // @ts-ignore - Parameter kept for future use
452
+ verbose: boolean
453
+ ): Promise<number> {
454
+ const sourceIntegrations = await listIntegrations(sourceClient);
455
+ const destIntegrations = await listIntegrations(destClient);
456
+ const destIntMap = new Map(destIntegrations.map(i => [i.idn, i]));
457
+
458
+ let connectorsCreated = 0;
459
+
460
+ for (const sourceInt of sourceIntegrations) {
461
+ const destInt = destIntMap.get(sourceInt.idn);
462
+ if (!destInt) continue;
463
+
464
+ const sourceConnectors = await listConnectors(sourceClient, sourceInt.id);
465
+ const destConnectors = await listConnectors(destClient, destInt.id);
466
+ const destConnMap = new Map(destConnectors.map(c => [c.connector_idn, c]));
467
+
468
+ for (const sourceConn of sourceConnectors) {
469
+ if (destConnMap.has(sourceConn.connector_idn)) continue;
470
+
471
+ try {
472
+ await createConnector(destClient, destInt.id, {
473
+ title: sourceConn.title,
474
+ connector_idn: sourceConn.connector_idn,
475
+ integration_idn: sourceInt.idn,
476
+ settings: sourceConn.settings
477
+ });
478
+ connectorsCreated++;
479
+ } catch (error: any) {
480
+ if (verbose && error.response?.status !== 409) {
481
+ console.error(` āš ļø Failed to create connector ${sourceConn.connector_idn}: ${error.message}`);
482
+ }
483
+ }
484
+ }
485
+ }
486
+
487
+ return connectorsCreated;
488
+ }
489
+
490
+ async function copyAccountFiles(sourceIdn: string, destIdn: string): Promise<void> {
491
+ const sourceDir = customerDir(sourceIdn);
492
+ const destDir = customerDir(destIdn);
493
+
494
+ await fs.ensureDir(destDir);
495
+
496
+ // Copy projects
497
+ const sourceProjects = path.join(sourceDir, 'projects');
498
+ const destProjects = path.join(destDir, 'projects');
499
+ if (await fs.pathExists(sourceProjects)) {
500
+ await fs.copy(sourceProjects, destProjects);
501
+ }
502
+
503
+ // Copy integrations
504
+ const sourceIntegrations = path.join(sourceDir, 'integrations');
505
+ const destIntegrations = path.join(destDir, 'integrations');
506
+ if (await fs.pathExists(sourceIntegrations)) {
507
+ await fs.copy(sourceIntegrations, destIntegrations);
508
+ }
509
+
510
+ // Copy AKB
511
+ const sourceAkb = path.join(sourceDir, 'akb');
512
+ const destAkb = path.join(destDir, 'akb');
513
+ if (await fs.pathExists(sourceAkb)) {
514
+ await fs.copy(sourceAkb, destAkb);
515
+ }
516
+
517
+ // Copy attributes
518
+ const sourceAttrs = path.join(sourceDir, 'attributes.yaml');
519
+ const destAttrs = path.join(destDir, 'attributes.yaml');
520
+ if (await fs.pathExists(sourceAttrs)) {
521
+ await fs.copy(sourceAttrs, destAttrs);
522
+ }
523
+ }
524
+
525
+ async function buildMapFromAPI(
526
+ destClient: AxiosInstance,
527
+ destCustomer: CustomerConfig,
528
+ // @ts-ignore - Parameter kept for future use
529
+ verbose: boolean
530
+ ): Promise<void> {
531
+ const newoDir = path.join('.newo', destCustomer.idn);
532
+ await fs.ensureDir(newoDir);
533
+
534
+ const projects = await listProjects(destClient);
535
+ const projectMap: any = { projects: {} };
536
+
537
+ for (const project of projects.filter(p => p.idn !== 'test')) {
538
+ const agents = await listAgents(destClient, project.id);
539
+ const projectData: any = {
540
+ projectId: project.id,
541
+ projectIdn: project.idn,
542
+ agents: {}
543
+ };
544
+
545
+ for (const agent of agents) {
546
+ projectData.agents[agent.idn] = {
547
+ id: agent.id,
548
+ flows: {}
549
+ };
550
+
551
+ for (const flow of agent.flows || []) {
552
+ const skills = await listFlowSkills(destClient, flow.id);
553
+ const skillMap: any = {};
554
+
555
+ for (const skill of skills) {
556
+ skillMap[skill.idn] = {
557
+ id: skill.id,
558
+ idn: skill.idn,
559
+ title: skill.title,
560
+ runner_type: skill.runner_type,
561
+ model: skill.model,
562
+ parameters: skill.parameters,
563
+ path: skill.path
564
+ };
565
+ }
566
+
567
+ projectData.agents[agent.idn].flows[flow.idn] = {
568
+ id: flow.id,
569
+ skills: skillMap
570
+ };
571
+ }
572
+ }
573
+
574
+ projectMap.projects[project.idn] = projectData;
575
+ }
576
+
577
+ await fs.writeJson(path.join(newoDir, 'map.json'), projectMap, { spaces: 2 });
578
+ await fs.writeJson(path.join(newoDir, 'hashes.json'), {}, { spaces: 2 });
579
+ }
580
+
581
+ async function pushSkillContent(
582
+ destClient: AxiosInstance,
583
+ destCustomer: CustomerConfig,
584
+ // @ts-ignore - Parameter kept for future use
585
+ verbose: boolean
586
+ ): Promise<number> {
587
+ const mapPath = path.join('.newo', destCustomer.idn, 'map.json');
588
+ const projectMap = await fs.readJson(mapPath) as any;
589
+ const destDir = customerDir(destCustomer.idn);
590
+
591
+ let pushedCount = 0;
592
+
593
+ for (const [projectIdn, projectData] of Object.entries(projectMap.projects || {})) {
594
+ const typedProjectData = projectData as any;
595
+
596
+ for (const [agentIdn, agentData] of Object.entries(typedProjectData.agents || {})) {
597
+ const typedAgentData = agentData as any;
598
+
599
+ for (const [flowIdn, flowData] of Object.entries(typedAgentData.flows || {})) {
600
+ const typedFlowData = flowData as any;
601
+
602
+ for (const [skillIdn, skillData] of Object.entries(typedFlowData.skills || {})) {
603
+ const typedSkillData = skillData as any;
604
+ const extension = typedSkillData.runner_type === 'nsl' ? 'jinja' : 'guidance';
605
+
606
+ const skillFilePath = path.join(
607
+ destDir,
608
+ 'projects',
609
+ projectIdn,
610
+ agentIdn,
611
+ flowIdn,
612
+ skillIdn,
613
+ `${skillIdn}.${extension}`
614
+ );
615
+
616
+ if (await fs.pathExists(skillFilePath)) {
617
+ const content = await fs.readFile(skillFilePath, 'utf8');
618
+
619
+ if (content.trim().length > 0) {
620
+ try {
621
+ await updateSkill(destClient, {
622
+ ...typedSkillData,
623
+ prompt_script: content
624
+ });
625
+ pushedCount++;
626
+
627
+ if (pushedCount % 100 === 0 && verbose) {
628
+ console.log(` Progress: ${pushedCount} skills pushed...`);
629
+ }
630
+ } catch (error: any) {
631
+ if (verbose) {
632
+ console.error(` āš ļø Failed to push ${skillIdn}: ${error.message}`);
633
+ }
634
+ }
635
+ }
636
+ }
637
+ }
638
+ }
639
+ }
640
+ }
641
+
642
+ return pushedCount;
643
+ }
644
+
645
+ async function createWebhooksFromYAML(
646
+ destClient: AxiosInstance,
647
+ destCustomer: CustomerConfig,
648
+ // @ts-ignore - Parameter kept for future use
649
+ verbose: boolean
650
+ ): Promise<number> {
651
+ const destDir = customerDir(destCustomer.idn);
652
+ let webhooksCreated = 0;
653
+
654
+ // Outgoing webhooks
655
+ const outgoingFile = path.join(destDir, 'integrations/api/connectors/webhook/webhooks/outgoing.yaml');
656
+ if (await fs.pathExists(outgoingFile)) {
657
+ const outgoingData = yaml.load(await fs.readFile(outgoingFile, 'utf8')) as any;
658
+ const webhooks = outgoingData.webhooks || [];
659
+
660
+ for (const webhook of webhooks) {
661
+ try {
662
+ await destClient.post('/api/v1/webhooks', {
663
+ idn: webhook.idn,
664
+ description: webhook.description || '',
665
+ connector_idn: webhook.connector_idn,
666
+ url: webhook.url,
667
+ command_idns: webhook.command_idns || []
668
+ });
669
+ webhooksCreated++;
670
+ } catch (error: any) {
671
+ if (error.response?.status !== 409 && verbose) {
672
+ console.error(` āš ļø Failed to create webhook ${webhook.idn}: ${error.message}`);
673
+ }
674
+ }
675
+ }
676
+ }
677
+
678
+ // Incoming webhooks
679
+ const incomingFile = path.join(destDir, 'integrations/api/connectors/webhook/webhooks/incoming.yaml');
680
+ if (await fs.pathExists(incomingFile)) {
681
+ const incomingData = yaml.load(await fs.readFile(incomingFile, 'utf8')) as any;
682
+ const webhooks = incomingData.webhooks || [];
683
+
684
+ for (const webhook of webhooks) {
685
+ try {
686
+ await destClient.post('/api/v1/webhooks/incoming', {
687
+ idn: webhook.idn,
688
+ description: webhook.description || '',
689
+ connector_idn: webhook.connector_idn,
690
+ event_idns: webhook.event_idns || [],
691
+ allowed_ips: webhook.allowed_ips || []
692
+ });
693
+ webhooksCreated++;
694
+ } catch (error: any) {
695
+ if (error.response?.status !== 409 && verbose) {
696
+ console.error(` āš ļø Failed to create webhook ${webhook.idn}: ${error.message}`);
697
+ }
698
+ }
699
+ }
700
+ }
701
+
702
+ return webhooksCreated;
703
+ }
704
+
705
+ async function verifyMigration(
706
+ sourceClient: AxiosInstance,
707
+ destClient: AxiosInstance,
708
+ // @ts-ignore - Parameter kept for API consistency
709
+ sourceCustomer: CustomerConfig,
710
+ // @ts-ignore - Parameter kept for API consistency
711
+ destCustomer: CustomerConfig
712
+ ): Promise<void> {
713
+ const sourceProjects = await listProjects(sourceClient);
714
+ const destProjects = await listProjects(destClient);
715
+
716
+ let srcSkills = 0;
717
+ let dstSkills = 0;
718
+
719
+ for (const proj of sourceProjects) {
720
+ const agents = await listAgents(sourceClient, proj.id);
721
+ for (const agent of agents) {
722
+ for (const flow of agent.flows || []) {
723
+ const skills = await listFlowSkills(sourceClient, flow.id);
724
+ srcSkills += skills.length;
725
+ }
726
+ }
727
+ }
728
+
729
+ for (const proj of destProjects.filter(p => p.idn !== 'test')) {
730
+ const agents = await listAgents(destClient, proj.id);
731
+ for (const agent of agents) {
732
+ for (const flow of agent.flows || []) {
733
+ const skills = await listFlowSkills(destClient, flow.id);
734
+ dstSkills += skills.length;
735
+ }
736
+ }
737
+ }
738
+
739
+ console.log(` Skills: ${srcSkills} source → ${dstSkills} destination ${srcSkills === dstSkills ? 'āœ…' : 'āŒ'}`);
740
+
741
+ if (srcSkills !== dstSkills) {
742
+ throw new Error(`Skill count mismatch: ${srcSkills} source vs ${dstSkills} destination`);
743
+ }
744
+
745
+ console.log(' āœ… Verification passed');
746
+ }