newo 3.4.0 → 3.4.2

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 (79) hide show
  1. package/CHANGELOG.md +16 -0
  2. package/dist/api.d.ts +3 -1
  3. package/dist/api.js +49 -1
  4. package/dist/application/migration/MigrationEngine.d.ts +141 -0
  5. package/dist/application/migration/MigrationEngine.js +322 -0
  6. package/dist/application/migration/index.d.ts +5 -0
  7. package/dist/application/migration/index.js +5 -0
  8. package/dist/application/sync/SyncEngine.d.ts +134 -0
  9. package/dist/application/sync/SyncEngine.js +335 -0
  10. package/dist/application/sync/index.d.ts +5 -0
  11. package/dist/application/sync/index.js +5 -0
  12. package/dist/cli/commands/create-attribute.js +1 -1
  13. package/dist/cli/commands/create-customer.d.ts +3 -0
  14. package/dist/cli/commands/create-customer.js +159 -0
  15. package/dist/cli/commands/diff.d.ts +6 -0
  16. package/dist/cli/commands/diff.js +288 -0
  17. package/dist/cli/commands/help.js +63 -3
  18. package/dist/cli/commands/logs.d.ts +18 -0
  19. package/dist/cli/commands/logs.js +283 -0
  20. package/dist/cli/commands/pull.js +114 -10
  21. package/dist/cli/commands/push.js +122 -12
  22. package/dist/cli/commands/update-attribute.d.ts +3 -0
  23. package/dist/cli/commands/update-attribute.js +78 -0
  24. package/dist/cli/commands/watch.d.ts +6 -0
  25. package/dist/cli/commands/watch.js +195 -0
  26. package/dist/cli-new/bootstrap.d.ts +74 -0
  27. package/dist/cli-new/bootstrap.js +154 -0
  28. package/dist/cli-new/di/Container.d.ts +64 -0
  29. package/dist/cli-new/di/Container.js +122 -0
  30. package/dist/cli-new/di/tokens.d.ts +77 -0
  31. package/dist/cli-new/di/tokens.js +76 -0
  32. package/dist/cli.js +20 -0
  33. package/dist/domain/resources/common/types.d.ts +71 -0
  34. package/dist/domain/resources/common/types.js +42 -0
  35. package/dist/domain/strategies/sync/AkbSyncStrategy.d.ts +63 -0
  36. package/dist/domain/strategies/sync/AkbSyncStrategy.js +274 -0
  37. package/dist/domain/strategies/sync/AttributeSyncStrategy.d.ts +87 -0
  38. package/dist/domain/strategies/sync/AttributeSyncStrategy.js +378 -0
  39. package/dist/domain/strategies/sync/ConversationSyncStrategy.d.ts +61 -0
  40. package/dist/domain/strategies/sync/ConversationSyncStrategy.js +232 -0
  41. package/dist/domain/strategies/sync/ISyncStrategy.d.ts +149 -0
  42. package/dist/domain/strategies/sync/ISyncStrategy.js +24 -0
  43. package/dist/domain/strategies/sync/IntegrationSyncStrategy.d.ts +68 -0
  44. package/dist/domain/strategies/sync/IntegrationSyncStrategy.js +413 -0
  45. package/dist/domain/strategies/sync/ProjectSyncStrategy.d.ts +111 -0
  46. package/dist/domain/strategies/sync/ProjectSyncStrategy.js +523 -0
  47. package/dist/domain/strategies/sync/index.d.ts +13 -0
  48. package/dist/domain/strategies/sync/index.js +19 -0
  49. package/dist/sync/migrate.js +99 -23
  50. package/dist/types.d.ts +124 -0
  51. package/package.json +3 -1
  52. package/src/api.ts +53 -2
  53. package/src/application/migration/MigrationEngine.ts +492 -0
  54. package/src/application/migration/index.ts +5 -0
  55. package/src/application/sync/SyncEngine.ts +467 -0
  56. package/src/application/sync/index.ts +5 -0
  57. package/src/cli/commands/create-attribute.ts +1 -1
  58. package/src/cli/commands/create-customer.ts +185 -0
  59. package/src/cli/commands/diff.ts +360 -0
  60. package/src/cli/commands/help.ts +63 -3
  61. package/src/cli/commands/logs.ts +329 -0
  62. package/src/cli/commands/pull.ts +128 -11
  63. package/src/cli/commands/push.ts +131 -13
  64. package/src/cli/commands/update-attribute.ts +82 -0
  65. package/src/cli/commands/watch.ts +227 -0
  66. package/src/cli-new/bootstrap.ts +252 -0
  67. package/src/cli-new/di/Container.ts +152 -0
  68. package/src/cli-new/di/tokens.ts +105 -0
  69. package/src/cli.ts +25 -0
  70. package/src/domain/resources/common/types.ts +106 -0
  71. package/src/domain/strategies/sync/AkbSyncStrategy.ts +358 -0
  72. package/src/domain/strategies/sync/AttributeSyncStrategy.ts +508 -0
  73. package/src/domain/strategies/sync/ConversationSyncStrategy.ts +299 -0
  74. package/src/domain/strategies/sync/ISyncStrategy.ts +182 -0
  75. package/src/domain/strategies/sync/IntegrationSyncStrategy.ts +522 -0
  76. package/src/domain/strategies/sync/ProjectSyncStrategy.ts +747 -0
  77. package/src/domain/strategies/sync/index.ts +46 -0
  78. package/src/sync/migrate.ts +103 -24
  79. package/src/types.ts +135 -0
@@ -0,0 +1,378 @@
1
+ /**
2
+ * AttributeSyncStrategy - Handles synchronization of Customer and Project Attributes
3
+ *
4
+ * This strategy implements ISyncStrategy for the Attributes resource.
5
+ *
6
+ * Key responsibilities:
7
+ * - Pull customer attributes from NEWO platform
8
+ * - Pull project attributes for all projects
9
+ * - Push changed attributes back to platform
10
+ * - Detect changes using stored hashes
11
+ */
12
+ import fs from 'fs-extra';
13
+ import yaml from 'js-yaml';
14
+ import path from 'path';
15
+ import { getCustomerAttributes, getProjectAttributes, updateCustomerAttribute, updateProjectAttribute, listProjects } from '../../../api.js';
16
+ import { writeFileSafe, customerAttributesPath, customerAttributesMapPath } from '../../../fsutil.js';
17
+ import { sha256, saveHashes, loadHashes } from '../../../hash.js';
18
+ /**
19
+ * AttributeSyncStrategy - Handles attribute synchronization
20
+ */
21
+ export class AttributeSyncStrategy {
22
+ apiClientFactory;
23
+ logger;
24
+ resourceType = 'attributes';
25
+ displayName = 'Attributes';
26
+ constructor(apiClientFactory, logger) {
27
+ this.apiClientFactory = apiClientFactory;
28
+ this.logger = logger;
29
+ }
30
+ /**
31
+ * Pull all attributes from NEWO platform
32
+ */
33
+ async pull(customer, options = {}) {
34
+ const client = await this.apiClientFactory(customer, options.verbose ?? false);
35
+ const hashes = {};
36
+ const items = [];
37
+ this.logger.verbose(`🔍 Fetching attributes for ${customer.idn}...`);
38
+ // Pull customer attributes
39
+ const customerAttrs = await this.pullCustomerAttributes(client, customer, hashes, options);
40
+ items.push(customerAttrs);
41
+ // Pull project attributes
42
+ const projects = await listProjects(client);
43
+ this.logger.verbose(`📁 Pulling attributes for ${projects.length} projects`);
44
+ for (const project of projects) {
45
+ try {
46
+ const projectAttrs = await this.pullProjectAttributes(client, customer, project.id, project.idn, hashes, options);
47
+ if (projectAttrs) {
48
+ items.push(projectAttrs);
49
+ }
50
+ }
51
+ catch (error) {
52
+ this.logger.warn(`Failed to pull attributes for project ${project.idn}`);
53
+ }
54
+ }
55
+ // Save hashes
56
+ const existingHashes = await loadHashes(customer.idn);
57
+ await saveHashes({ ...existingHashes, ...hashes }, customer.idn);
58
+ return {
59
+ items,
60
+ count: items.length,
61
+ hashes
62
+ };
63
+ }
64
+ /**
65
+ * Pull customer attributes
66
+ */
67
+ async pullCustomerAttributes(client, customer, hashes, options) {
68
+ this.logger.verbose(` 📦 Fetching customer attributes...`);
69
+ const response = await getCustomerAttributes(client, true);
70
+ const attributes = response.attributes || [];
71
+ // Create ID mapping
72
+ const idMapping = {};
73
+ const cleanAttributes = attributes.map(attr => {
74
+ if (attr.id) {
75
+ idMapping[attr.idn] = attr.id;
76
+ }
77
+ return this.cleanAttribute(attr);
78
+ });
79
+ // Format as YAML
80
+ const yamlContent = this.formatAttributesYaml(cleanAttributes);
81
+ // Save files
82
+ const attributesPath = customerAttributesPath(customer.idn);
83
+ await writeFileSafe(attributesPath, yamlContent);
84
+ await writeFileSafe(customerAttributesMapPath(customer.idn), JSON.stringify(idMapping, null, 2));
85
+ hashes[attributesPath] = sha256(yamlContent);
86
+ if (options.verbose) {
87
+ this.logger.info(` ✓ Saved ${cleanAttributes.length} customer attributes`);
88
+ }
89
+ return {
90
+ type: 'customer',
91
+ attributes: cleanAttributes,
92
+ idMapping
93
+ };
94
+ }
95
+ /**
96
+ * Pull project attributes
97
+ */
98
+ async pullProjectAttributes(client, customer, projectId, projectIdn, hashes, options) {
99
+ try {
100
+ const response = await getProjectAttributes(client, projectId, true);
101
+ const attributes = response.attributes || [];
102
+ if (attributes.length === 0) {
103
+ return null;
104
+ }
105
+ // Create ID mapping
106
+ const idMapping = {};
107
+ const cleanAttributes = attributes.map(attr => {
108
+ if (attr.id) {
109
+ idMapping[attr.idn] = attr.id;
110
+ }
111
+ return this.cleanAttribute(attr);
112
+ });
113
+ // Format as YAML
114
+ const yamlContent = this.formatAttributesYaml(cleanAttributes);
115
+ // Save files
116
+ const customerDir = path.join(process.cwd(), 'newo_customers', customer.idn);
117
+ const projectDir = path.join(customerDir, 'projects', projectIdn);
118
+ await fs.ensureDir(projectDir);
119
+ const attributesFile = path.join(projectDir, 'attributes.yaml');
120
+ const mapFile = path.join(process.cwd(), '.newo', customer.idn, `project_${projectIdn}_attributes-map.json`);
121
+ await writeFileSafe(attributesFile, yamlContent);
122
+ await fs.ensureDir(path.dirname(mapFile));
123
+ await writeFileSafe(mapFile, JSON.stringify(idMapping, null, 2));
124
+ hashes[attributesFile] = sha256(yamlContent);
125
+ if (options.verbose) {
126
+ this.logger.verbose(` ✓ Saved ${cleanAttributes.length} attributes for project ${projectIdn}`);
127
+ }
128
+ return {
129
+ type: 'project',
130
+ projectIdn,
131
+ attributes: cleanAttributes,
132
+ idMapping
133
+ };
134
+ }
135
+ catch (error) {
136
+ return null;
137
+ }
138
+ }
139
+ /**
140
+ * Clean an attribute for local storage
141
+ */
142
+ cleanAttribute(attr) {
143
+ let processedValue = attr.value;
144
+ // Handle JSON string values
145
+ if (typeof attr.value === 'string' && attr.value.startsWith('[{') && attr.value.endsWith('}]')) {
146
+ try {
147
+ const parsed = JSON.parse(attr.value);
148
+ processedValue = JSON.stringify(parsed, null, 0);
149
+ }
150
+ catch {
151
+ processedValue = attr.value;
152
+ }
153
+ }
154
+ return {
155
+ idn: attr.idn,
156
+ value: processedValue,
157
+ title: attr.title || '',
158
+ description: attr.description || '',
159
+ group: attr.group || '',
160
+ is_hidden: attr.is_hidden,
161
+ possible_values: attr.possible_values || [],
162
+ value_type: attr.value_type
163
+ };
164
+ }
165
+ /**
166
+ * Format attributes as YAML
167
+ */
168
+ formatAttributesYaml(attributes) {
169
+ // Add enum placeholders for value_type
170
+ const attributesWithPlaceholders = attributes.map(attr => ({
171
+ ...attr,
172
+ value_type: `__ENUM_PLACEHOLDER_${attr.value_type}__`
173
+ }));
174
+ let yamlContent = yaml.dump({ attributes: attributesWithPlaceholders }, {
175
+ indent: 2,
176
+ quotingType: '"',
177
+ forceQuotes: false,
178
+ lineWidth: 80,
179
+ noRefs: true,
180
+ sortKeys: false,
181
+ flowLevel: -1
182
+ });
183
+ // Replace placeholders with enum syntax
184
+ yamlContent = yamlContent.replace(/__ENUM_PLACEHOLDER_(\w+)__/g, '!enum "AttributeValueTypes.$1"');
185
+ yamlContent = yamlContent.replace(/\\"/g, '"');
186
+ return yamlContent;
187
+ }
188
+ /**
189
+ * Push changed attributes to NEWO platform
190
+ */
191
+ async push(customer, changes) {
192
+ const result = { created: 0, updated: 0, deleted: 0, errors: [] };
193
+ if (!changes) {
194
+ changes = await this.getChanges(customer);
195
+ }
196
+ if (changes.length === 0) {
197
+ return result;
198
+ }
199
+ const client = await this.apiClientFactory(customer, false);
200
+ for (const change of changes) {
201
+ try {
202
+ if (change.item.type === 'customer') {
203
+ const updateCount = await this.pushCustomerAttributes(client, customer, change.item);
204
+ result.updated += updateCount;
205
+ }
206
+ else if (change.item.type === 'project' && change.item.projectIdn) {
207
+ const updateCount = await this.pushProjectAttributes(client, customer, change.item.projectIdn, change.item);
208
+ result.updated += updateCount;
209
+ }
210
+ }
211
+ catch (error) {
212
+ result.errors.push(`Failed to push ${change.path}: ${error instanceof Error ? error.message : String(error)}`);
213
+ }
214
+ }
215
+ return result;
216
+ }
217
+ /**
218
+ * Push customer attributes
219
+ */
220
+ async pushCustomerAttributes(client, customer, _data) {
221
+ // Load current attributes from file
222
+ const attributesFile = customerAttributesPath(customer.idn);
223
+ const mapFile = customerAttributesMapPath(customer.idn);
224
+ if (!(await fs.pathExists(attributesFile)) || !(await fs.pathExists(mapFile))) {
225
+ return 0;
226
+ }
227
+ let content = await fs.readFile(attributesFile, 'utf-8');
228
+ content = content.replace(/!enum "AttributeValueTypes\.(\w+)"/g, '$1');
229
+ const localData = yaml.load(content);
230
+ const idMapping = JSON.parse(await fs.readFile(mapFile, 'utf-8'));
231
+ // Get remote attributes
232
+ const remoteResponse = await getCustomerAttributes(client, true);
233
+ const remoteMap = new Map();
234
+ remoteResponse.attributes.forEach(attr => remoteMap.set(attr.idn, attr));
235
+ let updatedCount = 0;
236
+ for (const localAttr of localData.attributes) {
237
+ const attributeId = idMapping[localAttr.idn];
238
+ if (!attributeId)
239
+ continue;
240
+ const remoteAttr = remoteMap.get(localAttr.idn);
241
+ if (!remoteAttr)
242
+ continue;
243
+ if (String(localAttr.value) !== String(remoteAttr.value)) {
244
+ await updateCustomerAttribute(client, {
245
+ id: attributeId,
246
+ ...localAttr
247
+ });
248
+ updatedCount++;
249
+ this.logger.info(` ✓ Updated customer attribute: ${localAttr.idn}`);
250
+ }
251
+ }
252
+ return updatedCount;
253
+ }
254
+ /**
255
+ * Push project attributes
256
+ */
257
+ async pushProjectAttributes(client, customer, projectIdn, _data) {
258
+ const customerDir = path.join(process.cwd(), 'newo_customers', customer.idn);
259
+ const attributesFile = path.join(customerDir, 'projects', projectIdn, 'attributes.yaml');
260
+ const mapFile = path.join(process.cwd(), '.newo', customer.idn, `project_${projectIdn}_attributes-map.json`);
261
+ if (!(await fs.pathExists(attributesFile)) || !(await fs.pathExists(mapFile))) {
262
+ return 0;
263
+ }
264
+ let content = await fs.readFile(attributesFile, 'utf-8');
265
+ content = content.replace(/!enum "AttributeValueTypes\.(\w+)"/g, '$1');
266
+ const localData = yaml.load(content);
267
+ const idMapping = JSON.parse(await fs.readFile(mapFile, 'utf-8'));
268
+ // Get project ID from projects list
269
+ const projects = await listProjects(client);
270
+ const project = projects.find(p => p.idn === projectIdn);
271
+ if (!project) {
272
+ return 0;
273
+ }
274
+ // Get remote attributes
275
+ const remoteResponse = await getProjectAttributes(client, project.id, true);
276
+ const remoteMap = new Map();
277
+ remoteResponse.attributes.forEach(attr => remoteMap.set(attr.idn, attr));
278
+ let updatedCount = 0;
279
+ for (const localAttr of localData.attributes) {
280
+ const attributeId = idMapping[localAttr.idn];
281
+ if (!attributeId)
282
+ continue;
283
+ const remoteAttr = remoteMap.get(localAttr.idn);
284
+ if (!remoteAttr)
285
+ continue;
286
+ if (String(localAttr.value) !== String(remoteAttr.value)) {
287
+ await updateProjectAttribute(client, project.id, {
288
+ id: attributeId,
289
+ ...localAttr
290
+ });
291
+ updatedCount++;
292
+ this.logger.info(` ✓ Updated project attribute: ${projectIdn}/${localAttr.idn}`);
293
+ }
294
+ }
295
+ return updatedCount;
296
+ }
297
+ /**
298
+ * Detect changes in attribute files
299
+ */
300
+ async getChanges(customer) {
301
+ const changes = [];
302
+ const hashes = await loadHashes(customer.idn);
303
+ // Check customer attributes
304
+ const customerAttrsPath = customerAttributesPath(customer.idn);
305
+ if (await fs.pathExists(customerAttrsPath)) {
306
+ const content = await fs.readFile(customerAttrsPath, 'utf-8');
307
+ const currentHash = sha256(content);
308
+ const storedHash = hashes[customerAttrsPath];
309
+ if (storedHash !== currentHash) {
310
+ changes.push({
311
+ item: { type: 'customer', attributes: [], idMapping: {} },
312
+ operation: 'modified',
313
+ path: customerAttrsPath
314
+ });
315
+ }
316
+ }
317
+ // Check project attributes
318
+ const customerDir = path.join(process.cwd(), 'newo_customers', customer.idn, 'projects');
319
+ if (await fs.pathExists(customerDir)) {
320
+ const projectDirs = await fs.readdir(customerDir);
321
+ for (const projectIdn of projectDirs) {
322
+ const attributesFile = path.join(customerDir, projectIdn, 'attributes.yaml');
323
+ if (await fs.pathExists(attributesFile)) {
324
+ const content = await fs.readFile(attributesFile, 'utf-8');
325
+ const currentHash = sha256(content);
326
+ const storedHash = hashes[attributesFile];
327
+ if (storedHash !== currentHash) {
328
+ changes.push({
329
+ item: { type: 'project', projectIdn, attributes: [], idMapping: {} },
330
+ operation: 'modified',
331
+ path: attributesFile
332
+ });
333
+ }
334
+ }
335
+ }
336
+ }
337
+ return changes;
338
+ }
339
+ /**
340
+ * Validate attribute data
341
+ */
342
+ async validate(_customer, items) {
343
+ const errors = [];
344
+ for (const item of items) {
345
+ for (const attr of item.attributes) {
346
+ if (!attr.idn) {
347
+ errors.push({
348
+ field: 'idn',
349
+ message: 'Attribute IDN is required'
350
+ });
351
+ }
352
+ }
353
+ }
354
+ return { valid: errors.length === 0, errors };
355
+ }
356
+ /**
357
+ * Get status summary
358
+ */
359
+ async getStatus(customer) {
360
+ const changes = await this.getChanges(customer);
361
+ return {
362
+ resourceType: this.resourceType,
363
+ displayName: this.displayName,
364
+ changedCount: changes.length,
365
+ changes: changes.map(c => ({
366
+ path: c.path,
367
+ operation: c.operation
368
+ }))
369
+ };
370
+ }
371
+ }
372
+ /**
373
+ * Factory function for creating AttributeSyncStrategy
374
+ */
375
+ export function createAttributeSyncStrategy(apiClientFactory, logger) {
376
+ return new AttributeSyncStrategy(apiClientFactory, logger);
377
+ }
378
+ //# sourceMappingURL=AttributeSyncStrategy.js.map
@@ -0,0 +1,61 @@
1
+ /**
2
+ * ConversationSyncStrategy - Handles synchronization of Conversation history
3
+ *
4
+ * This strategy implements ISyncStrategy for the Conversations resource.
5
+ * Note: This is a pull-only strategy as conversations are read-only.
6
+ *
7
+ * Key responsibilities:
8
+ * - Pull conversation history from NEWO platform
9
+ * - Process user personas and their acts
10
+ * - Save conversations to YAML format
11
+ */
12
+ import type { ISyncStrategy, PullOptions, PullResult, PushResult, ChangeItem, ValidationResult, StatusSummary } from './ISyncStrategy.js';
13
+ import type { CustomerConfig, ILogger } from '../../resources/common/types.js';
14
+ import type { AxiosInstance } from 'axios';
15
+ import type { UserPersona, ProcessedPersona } from '../../../types.js';
16
+ /**
17
+ * Local conversation data for storage
18
+ */
19
+ export interface LocalConversationData {
20
+ personas: ProcessedPersona[];
21
+ totalActs: number;
22
+ }
23
+ /**
24
+ * API client factory type
25
+ */
26
+ export type ApiClientFactory = (customer: CustomerConfig, verbose: boolean) => Promise<AxiosInstance>;
27
+ /**
28
+ * ConversationSyncStrategy - Handles conversation synchronization
29
+ */
30
+ export declare class ConversationSyncStrategy implements ISyncStrategy<UserPersona, LocalConversationData> {
31
+ private apiClientFactory;
32
+ private logger;
33
+ readonly resourceType = "conversations";
34
+ readonly displayName = "Conversations";
35
+ constructor(apiClientFactory: ApiClientFactory, logger: ILogger);
36
+ /**
37
+ * Pull all conversations from NEWO platform
38
+ */
39
+ pull(customer: CustomerConfig, options?: PullOptions): Promise<PullResult<LocalConversationData>>;
40
+ /**
41
+ * Push is not supported for conversations (read-only)
42
+ */
43
+ push(_customer: CustomerConfig, _changes?: ChangeItem<LocalConversationData>[]): Promise<PushResult>;
44
+ /**
45
+ * Get changes - conversations are typically regenerated on each pull
46
+ */
47
+ getChanges(_customer: CustomerConfig): Promise<ChangeItem<LocalConversationData>[]>;
48
+ /**
49
+ * Validate conversation data
50
+ */
51
+ validate(_customer: CustomerConfig, _items: LocalConversationData[]): Promise<ValidationResult>;
52
+ /**
53
+ * Get status summary
54
+ */
55
+ getStatus(customer: CustomerConfig): Promise<StatusSummary>;
56
+ }
57
+ /**
58
+ * Factory function for creating ConversationSyncStrategy
59
+ */
60
+ export declare function createConversationSyncStrategy(apiClientFactory: ApiClientFactory, logger: ILogger): ConversationSyncStrategy;
61
+ //# sourceMappingURL=ConversationSyncStrategy.d.ts.map
@@ -0,0 +1,232 @@
1
+ /**
2
+ * ConversationSyncStrategy - Handles synchronization of Conversation history
3
+ *
4
+ * This strategy implements ISyncStrategy for the Conversations resource.
5
+ * Note: This is a pull-only strategy as conversations are read-only.
6
+ *
7
+ * Key responsibilities:
8
+ * - Pull conversation history from NEWO platform
9
+ * - Process user personas and their acts
10
+ * - Save conversations to YAML format
11
+ */
12
+ import fs from 'fs-extra';
13
+ import yaml from 'js-yaml';
14
+ import path from 'path';
15
+ import pLimit from 'p-limit';
16
+ import { listUserPersonas, getChatHistory } from '../../../api.js';
17
+ import { sha256, saveHashes, loadHashes } from '../../../hash.js';
18
+ // Concurrency limit for API calls
19
+ const concurrencyLimit = pLimit(5);
20
+ /**
21
+ * ConversationSyncStrategy - Handles conversation synchronization
22
+ */
23
+ export class ConversationSyncStrategy {
24
+ apiClientFactory;
25
+ logger;
26
+ resourceType = 'conversations';
27
+ displayName = 'Conversations';
28
+ constructor(apiClientFactory, logger) {
29
+ this.apiClientFactory = apiClientFactory;
30
+ this.logger = logger;
31
+ }
32
+ /**
33
+ * Pull all conversations from NEWO platform
34
+ */
35
+ async pull(customer, options = {}) {
36
+ const client = await this.apiClientFactory(customer, options.verbose ?? false);
37
+ const hashes = {};
38
+ this.logger.verbose(`💬 Fetching conversations for ${customer.idn}...`);
39
+ const customerDir = path.join(process.cwd(), 'newo_customers', customer.idn);
40
+ await fs.ensureDir(customerDir);
41
+ // Get all user personas with pagination
42
+ const allPersonas = [];
43
+ let page = 1;
44
+ const perPage = 50;
45
+ let hasMore = true;
46
+ while (hasMore) {
47
+ const response = await listUserPersonas(client, page, perPage);
48
+ allPersonas.push(...response.items);
49
+ this.logger.verbose(` 📋 Page ${page}: Found ${response.items.length} personas (${allPersonas.length}/${response.metadata.total} total)`);
50
+ hasMore = response.items.length === perPage && allPersonas.length < response.metadata.total;
51
+ page++;
52
+ }
53
+ this.logger.verbose(`👥 Processing ${allPersonas.length} personas...`);
54
+ // Process personas concurrently with limited concurrency
55
+ const processedPersonas = [];
56
+ let totalActs = 0;
57
+ await Promise.all(allPersonas.map(persona => concurrencyLimit(async () => {
58
+ try {
59
+ // Extract phone number from actors
60
+ const phoneActor = persona.actors.find(actor => actor.integration_idn === 'newo_voice' &&
61
+ actor.connector_idn === 'newo_voice_connector' &&
62
+ actor.contact_information?.startsWith('+'));
63
+ const phone = phoneActor?.contact_information || null;
64
+ // Get user actor IDs from persona actors
65
+ const userActors = persona.actors.filter(actor => actor.integration_idn === 'newo_voice' &&
66
+ actor.connector_idn === 'newo_voice_connector');
67
+ if (userActors.length === 0) {
68
+ processedPersonas.push({
69
+ id: persona.id,
70
+ name: persona.name,
71
+ phone,
72
+ act_count: persona.act_count,
73
+ acts: []
74
+ });
75
+ return;
76
+ }
77
+ // Fetch chat history
78
+ const allActs = [];
79
+ let actPage = 1;
80
+ const actsPerPage = 100;
81
+ let hasMoreActs = true;
82
+ const maxPages = 50;
83
+ while (hasMoreActs && actPage <= maxPages) {
84
+ try {
85
+ const chatHistoryParams = {
86
+ user_actor_id: userActors[0].id,
87
+ page: actPage,
88
+ per: actsPerPage
89
+ };
90
+ const chatResponse = await getChatHistory(client, chatHistoryParams);
91
+ if (chatResponse.items && chatResponse.items.length > 0) {
92
+ const convertedActs = chatResponse.items.map((item) => ({
93
+ id: item.id || `chat_${Math.random()}`,
94
+ command_act_id: null,
95
+ external_event_id: item.external_event_id || 'chat_history',
96
+ arguments: [],
97
+ reference_idn: item.is_agent === true ? 'agent_message' : 'user_message',
98
+ runtime_context_id: item.runtime_context_id || 'chat_history',
99
+ source_text: item.payload?.text || item.message || '',
100
+ original_text: item.payload?.text || item.message || '',
101
+ datetime: item.datetime || item.created_at || new Date().toISOString(),
102
+ user_actor_id: userActors[0].id,
103
+ agent_actor_id: null,
104
+ user_persona_id: persona.id,
105
+ user_persona_name: persona.name,
106
+ agent_persona_id: item.agent_persona_id || 'unknown',
107
+ external_id: item.external_id || null,
108
+ integration_idn: 'newo_voice',
109
+ connector_idn: 'newo_voice_connector',
110
+ to_integration_idn: null,
111
+ to_connector_idn: null,
112
+ is_agent: Boolean(item.is_agent === true),
113
+ project_idn: null,
114
+ flow_idn: item.flow_idn || 'unknown',
115
+ skill_idn: item.skill_idn || 'unknown',
116
+ session_id: item.session_id || 'unknown',
117
+ recordings: item.recordings || [],
118
+ contact_information: item.contact_information || null
119
+ }));
120
+ allActs.push(...convertedActs);
121
+ hasMoreActs = chatResponse.items.length === actsPerPage &&
122
+ (!chatResponse.metadata?.total || allActs.length < chatResponse.metadata.total);
123
+ actPage++;
124
+ }
125
+ else {
126
+ hasMoreActs = false;
127
+ }
128
+ }
129
+ catch (_error) {
130
+ hasMoreActs = false;
131
+ }
132
+ }
133
+ // Process acts for YAML output
134
+ const processedActs = allActs
135
+ .sort((a, b) => new Date(a.datetime).getTime() - new Date(b.datetime).getTime())
136
+ .map(act => ({
137
+ datetime: act.datetime,
138
+ type: act.is_agent ? 'agent' : 'user',
139
+ message: act.source_text,
140
+ contact_information: act.contact_information,
141
+ flow_idn: act.flow_idn,
142
+ skill_idn: act.skill_idn,
143
+ session_id: act.session_id
144
+ }));
145
+ processedPersonas.push({
146
+ id: persona.id,
147
+ name: persona.name,
148
+ phone,
149
+ act_count: persona.act_count,
150
+ acts: processedActs
151
+ });
152
+ totalActs += processedActs.length;
153
+ this.logger.verbose(` ✓ Processed ${persona.name}: ${processedActs.length} acts`);
154
+ }
155
+ catch (error) {
156
+ this.logger.warn(`Failed to process persona ${persona.name}`);
157
+ }
158
+ })));
159
+ // Sort personas by most recent activity
160
+ processedPersonas.sort((a, b) => {
161
+ const aLastAct = a.acts[a.acts.length - 1]?.datetime;
162
+ const bLastAct = b.acts[b.acts.length - 1]?.datetime;
163
+ if (!aLastAct && !bLastAct)
164
+ return 0;
165
+ if (!aLastAct)
166
+ return 1;
167
+ if (!bLastAct)
168
+ return -1;
169
+ return new Date(bLastAct).getTime() - new Date(aLastAct).getTime();
170
+ });
171
+ // Save to YAML
172
+ const conversationsFile = path.join(customerDir, 'conversations.yaml');
173
+ const yamlContent = yaml.dump({ personas: processedPersonas }, { lineWidth: -1 });
174
+ await fs.writeFile(conversationsFile, yamlContent);
175
+ hashes[conversationsFile] = sha256(yamlContent);
176
+ // Save hashes
177
+ const existingHashes = await loadHashes(customer.idn);
178
+ await saveHashes({ ...existingHashes, ...hashes }, customer.idn);
179
+ this.logger.info(`✅ Saved ${processedPersonas.length} personas with ${totalActs} conversation acts`);
180
+ return {
181
+ items: [{
182
+ personas: processedPersonas,
183
+ totalActs
184
+ }],
185
+ count: 1,
186
+ hashes
187
+ };
188
+ }
189
+ /**
190
+ * Push is not supported for conversations (read-only)
191
+ */
192
+ async push(_customer, _changes) {
193
+ this.logger.warn('Conversations are read-only and cannot be pushed');
194
+ return { created: 0, updated: 0, deleted: 0, errors: ['Conversations are read-only'] };
195
+ }
196
+ /**
197
+ * Get changes - conversations are typically regenerated on each pull
198
+ */
199
+ async getChanges(_customer) {
200
+ // Conversations don't support change detection in the traditional sense
201
+ // They are regenerated on each pull
202
+ return [];
203
+ }
204
+ /**
205
+ * Validate conversation data
206
+ */
207
+ async validate(_customer, _items) {
208
+ // Conversations are read-only, no validation needed
209
+ return { valid: true, errors: [] };
210
+ }
211
+ /**
212
+ * Get status summary
213
+ */
214
+ async getStatus(customer) {
215
+ const customerDir = path.join(process.cwd(), 'newo_customers', customer.idn);
216
+ const conversationsFile = path.join(customerDir, 'conversations.yaml');
217
+ const exists = await fs.pathExists(conversationsFile);
218
+ return {
219
+ resourceType: this.resourceType,
220
+ displayName: this.displayName,
221
+ changedCount: 0,
222
+ changes: exists ? [] : [{ path: conversationsFile, operation: 'created' }]
223
+ };
224
+ }
225
+ }
226
+ /**
227
+ * Factory function for creating ConversationSyncStrategy
228
+ */
229
+ export function createConversationSyncStrategy(apiClientFactory, logger) {
230
+ return new ConversationSyncStrategy(apiClientFactory, logger);
231
+ }
232
+ //# sourceMappingURL=ConversationSyncStrategy.js.map