newo 3.3.3 → 3.4.1

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 (83) hide show
  1. package/CHANGELOG.md +41 -0
  2. package/dist/api.d.ts +6 -1
  3. package/dist/api.js +63 -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/add-project.d.ts +3 -0
  13. package/dist/cli/commands/add-project.js +136 -0
  14. package/dist/cli/commands/create-customer.d.ts +3 -0
  15. package/dist/cli/commands/create-customer.js +159 -0
  16. package/dist/cli/commands/diff.d.ts +6 -0
  17. package/dist/cli/commands/diff.js +288 -0
  18. package/dist/cli/commands/help.js +75 -4
  19. package/dist/cli/commands/list-registries.d.ts +3 -0
  20. package/dist/cli/commands/list-registries.js +39 -0
  21. package/dist/cli/commands/list-registry-items.d.ts +3 -0
  22. package/dist/cli/commands/list-registry-items.js +112 -0
  23. package/dist/cli/commands/logs.d.ts +18 -0
  24. package/dist/cli/commands/logs.js +283 -0
  25. package/dist/cli/commands/pull.js +114 -10
  26. package/dist/cli/commands/push.js +122 -12
  27. package/dist/cli/commands/watch.d.ts +6 -0
  28. package/dist/cli/commands/watch.js +195 -0
  29. package/dist/cli-new/bootstrap.d.ts +74 -0
  30. package/dist/cli-new/bootstrap.js +154 -0
  31. package/dist/cli-new/di/Container.d.ts +64 -0
  32. package/dist/cli-new/di/Container.js +122 -0
  33. package/dist/cli-new/di/tokens.d.ts +77 -0
  34. package/dist/cli-new/di/tokens.js +76 -0
  35. package/dist/cli.js +28 -0
  36. package/dist/domain/resources/common/types.d.ts +71 -0
  37. package/dist/domain/resources/common/types.js +42 -0
  38. package/dist/domain/strategies/sync/AkbSyncStrategy.d.ts +63 -0
  39. package/dist/domain/strategies/sync/AkbSyncStrategy.js +274 -0
  40. package/dist/domain/strategies/sync/AttributeSyncStrategy.d.ts +87 -0
  41. package/dist/domain/strategies/sync/AttributeSyncStrategy.js +378 -0
  42. package/dist/domain/strategies/sync/ConversationSyncStrategy.d.ts +61 -0
  43. package/dist/domain/strategies/sync/ConversationSyncStrategy.js +232 -0
  44. package/dist/domain/strategies/sync/ISyncStrategy.d.ts +149 -0
  45. package/dist/domain/strategies/sync/ISyncStrategy.js +24 -0
  46. package/dist/domain/strategies/sync/IntegrationSyncStrategy.d.ts +68 -0
  47. package/dist/domain/strategies/sync/IntegrationSyncStrategy.js +413 -0
  48. package/dist/domain/strategies/sync/ProjectSyncStrategy.d.ts +111 -0
  49. package/dist/domain/strategies/sync/ProjectSyncStrategy.js +523 -0
  50. package/dist/domain/strategies/sync/index.d.ts +13 -0
  51. package/dist/domain/strategies/sync/index.js +19 -0
  52. package/dist/sync/migrate.js +99 -23
  53. package/dist/types.d.ts +162 -0
  54. package/package.json +3 -1
  55. package/src/api.ts +77 -2
  56. package/src/application/migration/MigrationEngine.ts +492 -0
  57. package/src/application/migration/index.ts +5 -0
  58. package/src/application/sync/SyncEngine.ts +467 -0
  59. package/src/application/sync/index.ts +5 -0
  60. package/src/cli/commands/add-project.ts +159 -0
  61. package/src/cli/commands/create-customer.ts +185 -0
  62. package/src/cli/commands/diff.ts +360 -0
  63. package/src/cli/commands/help.ts +75 -4
  64. package/src/cli/commands/list-registries.ts +53 -0
  65. package/src/cli/commands/list-registry-items.ts +149 -0
  66. package/src/cli/commands/logs.ts +329 -0
  67. package/src/cli/commands/pull.ts +128 -11
  68. package/src/cli/commands/push.ts +131 -13
  69. package/src/cli/commands/watch.ts +227 -0
  70. package/src/cli-new/bootstrap.ts +252 -0
  71. package/src/cli-new/di/Container.ts +152 -0
  72. package/src/cli-new/di/tokens.ts +105 -0
  73. package/src/cli.ts +35 -0
  74. package/src/domain/resources/common/types.ts +106 -0
  75. package/src/domain/strategies/sync/AkbSyncStrategy.ts +358 -0
  76. package/src/domain/strategies/sync/AttributeSyncStrategy.ts +508 -0
  77. package/src/domain/strategies/sync/ConversationSyncStrategy.ts +299 -0
  78. package/src/domain/strategies/sync/ISyncStrategy.ts +182 -0
  79. package/src/domain/strategies/sync/IntegrationSyncStrategy.ts +522 -0
  80. package/src/domain/strategies/sync/ProjectSyncStrategy.ts +747 -0
  81. package/src/domain/strategies/sync/index.ts +46 -0
  82. package/src/sync/migrate.ts +103 -24
  83. package/src/types.ts +178 -0
@@ -0,0 +1,299 @@
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
+
13
+ import type {
14
+ ISyncStrategy,
15
+ PullOptions,
16
+ PullResult,
17
+ PushResult,
18
+ ChangeItem,
19
+ ValidationResult,
20
+ StatusSummary
21
+ } from './ISyncStrategy.js';
22
+ import type { CustomerConfig, ILogger, HashStore } from '../../resources/common/types.js';
23
+ import type { AxiosInstance } from 'axios';
24
+ import type {
25
+ UserPersona,
26
+ ConversationAct,
27
+ ProcessedPersona,
28
+ ProcessedAct
29
+ } from '../../../types.js';
30
+ import fs from 'fs-extra';
31
+ import yaml from 'js-yaml';
32
+ import path from 'path';
33
+ import pLimit from 'p-limit';
34
+ import { listUserPersonas, getChatHistory } from '../../../api.js';
35
+ import { sha256, saveHashes, loadHashes } from '../../../hash.js';
36
+
37
+ // Concurrency limit for API calls
38
+ const concurrencyLimit = pLimit(5);
39
+
40
+ /**
41
+ * Local conversation data for storage
42
+ */
43
+ export interface LocalConversationData {
44
+ personas: ProcessedPersona[];
45
+ totalActs: number;
46
+ }
47
+
48
+ /**
49
+ * API client factory type
50
+ */
51
+ export type ApiClientFactory = (customer: CustomerConfig, verbose: boolean) => Promise<AxiosInstance>;
52
+
53
+ /**
54
+ * ConversationSyncStrategy - Handles conversation synchronization
55
+ */
56
+ export class ConversationSyncStrategy implements ISyncStrategy<UserPersona, LocalConversationData> {
57
+ readonly resourceType = 'conversations';
58
+ readonly displayName = 'Conversations';
59
+
60
+ constructor(
61
+ private apiClientFactory: ApiClientFactory,
62
+ private logger: ILogger
63
+ ) {}
64
+
65
+ /**
66
+ * Pull all conversations from NEWO platform
67
+ */
68
+ async pull(customer: CustomerConfig, options: PullOptions = {}): Promise<PullResult<LocalConversationData>> {
69
+ const client = await this.apiClientFactory(customer, options.verbose ?? false);
70
+ const hashes: HashStore = {};
71
+
72
+ this.logger.verbose(`💬 Fetching conversations for ${customer.idn}...`);
73
+
74
+ const customerDir = path.join(process.cwd(), 'newo_customers', customer.idn);
75
+ await fs.ensureDir(customerDir);
76
+
77
+ // Get all user personas with pagination
78
+ const allPersonas: UserPersona[] = [];
79
+ let page = 1;
80
+ const perPage = 50;
81
+ let hasMore = true;
82
+
83
+ while (hasMore) {
84
+ const response = await listUserPersonas(client, page, perPage);
85
+ allPersonas.push(...response.items);
86
+
87
+ this.logger.verbose(` 📋 Page ${page}: Found ${response.items.length} personas (${allPersonas.length}/${response.metadata.total} total)`);
88
+
89
+ hasMore = response.items.length === perPage && allPersonas.length < response.metadata.total;
90
+ page++;
91
+ }
92
+
93
+ this.logger.verbose(`👥 Processing ${allPersonas.length} personas...`);
94
+
95
+ // Process personas concurrently with limited concurrency
96
+ const processedPersonas: ProcessedPersona[] = [];
97
+ let totalActs = 0;
98
+
99
+ await Promise.all(allPersonas.map(persona => concurrencyLimit(async () => {
100
+ try {
101
+ // Extract phone number from actors
102
+ const phoneActor = persona.actors.find(actor =>
103
+ actor.integration_idn === 'newo_voice' &&
104
+ actor.connector_idn === 'newo_voice_connector' &&
105
+ actor.contact_information?.startsWith('+')
106
+ );
107
+ const phone = phoneActor?.contact_information || null;
108
+
109
+ // Get user actor IDs from persona actors
110
+ const userActors = persona.actors.filter(actor =>
111
+ actor.integration_idn === 'newo_voice' &&
112
+ actor.connector_idn === 'newo_voice_connector'
113
+ );
114
+
115
+ if (userActors.length === 0) {
116
+ processedPersonas.push({
117
+ id: persona.id,
118
+ name: persona.name,
119
+ phone,
120
+ act_count: persona.act_count,
121
+ acts: []
122
+ });
123
+ return;
124
+ }
125
+
126
+ // Fetch chat history
127
+ const allActs: ConversationAct[] = [];
128
+ let actPage = 1;
129
+ const actsPerPage = 100;
130
+ let hasMoreActs = true;
131
+ const maxPages = 50;
132
+
133
+ while (hasMoreActs && actPage <= maxPages) {
134
+ try {
135
+ const chatHistoryParams = {
136
+ user_actor_id: userActors[0]!.id,
137
+ page: actPage,
138
+ per: actsPerPage
139
+ };
140
+
141
+ const chatResponse = await getChatHistory(client, chatHistoryParams);
142
+
143
+ if (chatResponse.items && chatResponse.items.length > 0) {
144
+ const convertedActs: ConversationAct[] = chatResponse.items.map((item: Record<string, unknown>) => ({
145
+ id: (item.id as string) || `chat_${Math.random()}`,
146
+ command_act_id: null,
147
+ external_event_id: (item.external_event_id as string) || 'chat_history',
148
+ arguments: [],
149
+ reference_idn: item.is_agent === true ? 'agent_message' : 'user_message',
150
+ runtime_context_id: (item.runtime_context_id as string) || 'chat_history',
151
+ source_text: (item.payload as Record<string, unknown>)?.text as string || (item.message as string) || '',
152
+ original_text: (item.payload as Record<string, unknown>)?.text as string || (item.message as string) || '',
153
+ datetime: (item.datetime as string) || (item.created_at as string) || new Date().toISOString(),
154
+ user_actor_id: userActors[0]!.id,
155
+ agent_actor_id: null,
156
+ user_persona_id: persona.id,
157
+ user_persona_name: persona.name,
158
+ agent_persona_id: (item.agent_persona_id as string) || 'unknown',
159
+ external_id: (item.external_id as string) || null,
160
+ integration_idn: 'newo_voice',
161
+ connector_idn: 'newo_voice_connector',
162
+ to_integration_idn: null,
163
+ to_connector_idn: null,
164
+ is_agent: Boolean(item.is_agent === true),
165
+ project_idn: null,
166
+ flow_idn: (item.flow_idn as string) || 'unknown',
167
+ skill_idn: (item.skill_idn as string) || 'unknown',
168
+ session_id: (item.session_id as string) || 'unknown',
169
+ recordings: (item.recordings as unknown[]) || [],
170
+ contact_information: (item.contact_information as string) || null
171
+ }));
172
+
173
+ allActs.push(...convertedActs);
174
+
175
+ hasMoreActs = chatResponse.items.length === actsPerPage &&
176
+ (!chatResponse.metadata?.total || allActs.length < chatResponse.metadata.total);
177
+ actPage++;
178
+ } else {
179
+ hasMoreActs = false;
180
+ }
181
+ } catch (_error) {
182
+ hasMoreActs = false;
183
+ }
184
+ }
185
+
186
+ // Process acts for YAML output
187
+ const processedActs: ProcessedAct[] = allActs
188
+ .sort((a, b) => new Date(a.datetime).getTime() - new Date(b.datetime).getTime())
189
+ .map(act => ({
190
+ datetime: act.datetime,
191
+ type: act.is_agent ? 'agent' : 'user',
192
+ message: act.source_text,
193
+ contact_information: act.contact_information,
194
+ flow_idn: act.flow_idn,
195
+ skill_idn: act.skill_idn,
196
+ session_id: act.session_id
197
+ }));
198
+
199
+ processedPersonas.push({
200
+ id: persona.id,
201
+ name: persona.name,
202
+ phone,
203
+ act_count: persona.act_count,
204
+ acts: processedActs
205
+ });
206
+
207
+ totalActs += processedActs.length;
208
+
209
+ this.logger.verbose(` ✓ Processed ${persona.name}: ${processedActs.length} acts`);
210
+ } catch (error) {
211
+ this.logger.warn(`Failed to process persona ${persona.name}`);
212
+ }
213
+ })));
214
+
215
+ // Sort personas by most recent activity
216
+ processedPersonas.sort((a, b) => {
217
+ const aLastAct = a.acts[a.acts.length - 1]?.datetime;
218
+ const bLastAct = b.acts[b.acts.length - 1]?.datetime;
219
+ if (!aLastAct && !bLastAct) return 0;
220
+ if (!aLastAct) return 1;
221
+ if (!bLastAct) return -1;
222
+ return new Date(bLastAct).getTime() - new Date(aLastAct).getTime();
223
+ });
224
+
225
+ // Save to YAML
226
+ const conversationsFile = path.join(customerDir, 'conversations.yaml');
227
+ const yamlContent = yaml.dump({ personas: processedPersonas }, { lineWidth: -1 });
228
+ await fs.writeFile(conversationsFile, yamlContent);
229
+
230
+ hashes[conversationsFile] = sha256(yamlContent);
231
+
232
+ // Save hashes
233
+ const existingHashes = await loadHashes(customer.idn);
234
+ await saveHashes({ ...existingHashes, ...hashes }, customer.idn);
235
+
236
+ this.logger.info(`✅ Saved ${processedPersonas.length} personas with ${totalActs} conversation acts`);
237
+
238
+ return {
239
+ items: [{
240
+ personas: processedPersonas,
241
+ totalActs
242
+ }],
243
+ count: 1,
244
+ hashes
245
+ };
246
+ }
247
+
248
+ /**
249
+ * Push is not supported for conversations (read-only)
250
+ */
251
+ async push(_customer: CustomerConfig, _changes?: ChangeItem<LocalConversationData>[]): Promise<PushResult> {
252
+ this.logger.warn('Conversations are read-only and cannot be pushed');
253
+ return { created: 0, updated: 0, deleted: 0, errors: ['Conversations are read-only'] };
254
+ }
255
+
256
+ /**
257
+ * Get changes - conversations are typically regenerated on each pull
258
+ */
259
+ async getChanges(_customer: CustomerConfig): Promise<ChangeItem<LocalConversationData>[]> {
260
+ // Conversations don't support change detection in the traditional sense
261
+ // They are regenerated on each pull
262
+ return [];
263
+ }
264
+
265
+ /**
266
+ * Validate conversation data
267
+ */
268
+ async validate(_customer: CustomerConfig, _items: LocalConversationData[]): Promise<ValidationResult> {
269
+ // Conversations are read-only, no validation needed
270
+ return { valid: true, errors: [] };
271
+ }
272
+
273
+ /**
274
+ * Get status summary
275
+ */
276
+ async getStatus(customer: CustomerConfig): Promise<StatusSummary> {
277
+ const customerDir = path.join(process.cwd(), 'newo_customers', customer.idn);
278
+ const conversationsFile = path.join(customerDir, 'conversations.yaml');
279
+
280
+ const exists = await fs.pathExists(conversationsFile);
281
+
282
+ return {
283
+ resourceType: this.resourceType,
284
+ displayName: this.displayName,
285
+ changedCount: 0,
286
+ changes: exists ? [] : [{ path: conversationsFile, operation: 'created' }]
287
+ };
288
+ }
289
+ }
290
+
291
+ /**
292
+ * Factory function for creating ConversationSyncStrategy
293
+ */
294
+ export function createConversationSyncStrategy(
295
+ apiClientFactory: ApiClientFactory,
296
+ logger: ILogger
297
+ ): ConversationSyncStrategy {
298
+ return new ConversationSyncStrategy(apiClientFactory, logger);
299
+ }
@@ -0,0 +1,182 @@
1
+ /**
2
+ * Generic Sync Strategy Interface
3
+ *
4
+ * All resource types (Projects, Integrations, AKB, Attributes) implement this interface.
5
+ * This enables the SyncEngine to handle all resources uniformly.
6
+ */
7
+
8
+ import type { CustomerConfig } from '../../resources/common/types.js';
9
+
10
+ /**
11
+ * Validation result for pre-sync validation
12
+ */
13
+ export interface ValidationResult {
14
+ valid: boolean;
15
+ errors: ValidationError[];
16
+ }
17
+
18
+ export interface ValidationError {
19
+ field: string;
20
+ message: string;
21
+ path?: string;
22
+ }
23
+
24
+ /**
25
+ * Change operation types
26
+ */
27
+ export type ChangeOperation = 'created' | 'modified' | 'deleted';
28
+
29
+ /**
30
+ * Generic change item representing a resource change
31
+ */
32
+ export interface ChangeItem<T = unknown> {
33
+ item: T;
34
+ operation: ChangeOperation;
35
+ path: string;
36
+ }
37
+
38
+ /**
39
+ * Pull result containing resources and metadata
40
+ */
41
+ export interface PullResult<T = unknown> {
42
+ items: T[];
43
+ count: number;
44
+ hashes: Record<string, string>;
45
+ }
46
+
47
+ /**
48
+ * Push result containing operation outcomes
49
+ */
50
+ export interface PushResult {
51
+ created: number;
52
+ updated: number;
53
+ deleted: number;
54
+ errors: string[];
55
+ }
56
+
57
+ /**
58
+ * Generic Sync Strategy Interface
59
+ *
60
+ * TRemote - Type from the API (e.g., API response types)
61
+ * TLocal - Type for local storage (e.g., YAML/JSON file types)
62
+ */
63
+ export interface ISyncStrategy<_TRemote = unknown, TLocal = unknown> {
64
+ /**
65
+ * Resource type identifier (e.g., 'projects', 'integrations', 'akb', 'attributes')
66
+ */
67
+ readonly resourceType: string;
68
+
69
+ /**
70
+ * Display name for logging/UI (e.g., 'Projects', 'Integrations')
71
+ */
72
+ readonly displayName: string;
73
+
74
+ /**
75
+ * Pull resources from NEWO platform to local filesystem
76
+ *
77
+ * @param customer - Customer configuration
78
+ * @param options - Optional pull options
79
+ * @returns Pull result with items and hashes
80
+ */
81
+ pull(customer: CustomerConfig, options?: PullOptions): Promise<PullResult<TLocal>>;
82
+
83
+ /**
84
+ * Push local changes to NEWO platform
85
+ *
86
+ * @param customer - Customer configuration
87
+ * @param changes - Changes to push (if not provided, detect changes automatically)
88
+ * @returns Push result with counts
89
+ */
90
+ push(customer: CustomerConfig, changes?: ChangeItem<TLocal>[]): Promise<PushResult>;
91
+
92
+ /**
93
+ * Detect what has changed locally since last sync
94
+ *
95
+ * @param customer - Customer configuration
96
+ * @returns Array of changed items
97
+ */
98
+ getChanges(customer: CustomerConfig): Promise<ChangeItem<TLocal>[]>;
99
+
100
+ /**
101
+ * Validate local state before push
102
+ *
103
+ * @param customer - Customer configuration
104
+ * @param items - Items to validate
105
+ * @returns Validation result
106
+ */
107
+ validate(customer: CustomerConfig, items: TLocal[]): Promise<ValidationResult>;
108
+
109
+ /**
110
+ * Get status summary for display
111
+ *
112
+ * @param customer - Customer configuration
113
+ * @returns Status summary
114
+ */
115
+ getStatus(customer: CustomerConfig): Promise<StatusSummary>;
116
+ }
117
+
118
+ /**
119
+ * Pull operation options
120
+ */
121
+ export interface PullOptions {
122
+ /**
123
+ * Overwrite local changes without prompting
124
+ */
125
+ silentOverwrite?: boolean;
126
+
127
+ /**
128
+ * Enable verbose logging
129
+ */
130
+ verbose?: boolean;
131
+
132
+ /**
133
+ * Specific project ID to pull (for projects strategy)
134
+ */
135
+ projectId?: string | null;
136
+
137
+ /**
138
+ * Skip deletion detection and cleanup
139
+ */
140
+ skipCleanup?: boolean;
141
+ }
142
+
143
+ /**
144
+ * Status summary for a resource type
145
+ */
146
+ export interface StatusSummary {
147
+ resourceType: string;
148
+ displayName: string;
149
+ changedCount: number;
150
+ changes: Array<{
151
+ path: string;
152
+ operation: ChangeOperation;
153
+ details?: string;
154
+ }>;
155
+ }
156
+
157
+ /**
158
+ * Abstract base class with common strategy functionality
159
+ */
160
+ export abstract class BaseSyncStrategy<TRemote = unknown, TLocal = unknown> implements ISyncStrategy<TRemote, TLocal> {
161
+ abstract readonly resourceType: string;
162
+ abstract readonly displayName: string;
163
+
164
+ abstract pull(customer: CustomerConfig, options?: PullOptions): Promise<PullResult<TLocal>>;
165
+ abstract push(customer: CustomerConfig, changes?: ChangeItem<TLocal>[]): Promise<PushResult>;
166
+ abstract getChanges(customer: CustomerConfig): Promise<ChangeItem<TLocal>[]>;
167
+ abstract validate(customer: CustomerConfig, items: TLocal[]): Promise<ValidationResult>;
168
+
169
+ async getStatus(customer: CustomerConfig): Promise<StatusSummary> {
170
+ const changes = await this.getChanges(customer);
171
+
172
+ return {
173
+ resourceType: this.resourceType,
174
+ displayName: this.displayName,
175
+ changedCount: changes.length,
176
+ changes: changes.map(c => ({
177
+ path: c.path,
178
+ operation: c.operation
179
+ }))
180
+ };
181
+ }
182
+ }