newo 1.9.1 → 2.0.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (66) hide show
  1. package/CHANGELOG.md +131 -0
  2. package/README.md +68 -20
  3. package/dist/cli/commands/conversations.d.ts +3 -0
  4. package/dist/cli/commands/conversations.js +38 -0
  5. package/dist/cli/commands/help.d.ts +5 -0
  6. package/dist/cli/commands/help.js +50 -0
  7. package/dist/cli/commands/import-akb.d.ts +3 -0
  8. package/dist/cli/commands/import-akb.js +62 -0
  9. package/dist/cli/commands/list-customers.d.ts +3 -0
  10. package/dist/cli/commands/list-customers.js +13 -0
  11. package/dist/cli/commands/meta.d.ts +3 -0
  12. package/dist/cli/commands/meta.js +19 -0
  13. package/dist/cli/commands/pull-attributes.d.ts +3 -0
  14. package/dist/cli/commands/pull-attributes.js +16 -0
  15. package/dist/cli/commands/pull.d.ts +3 -0
  16. package/dist/cli/commands/pull.js +34 -0
  17. package/dist/cli/commands/push.d.ts +3 -0
  18. package/dist/cli/commands/push.js +39 -0
  19. package/dist/cli/commands/status.d.ts +3 -0
  20. package/dist/cli/commands/status.js +22 -0
  21. package/dist/cli/customer-selection.d.ts +23 -0
  22. package/dist/cli/customer-selection.js +110 -0
  23. package/dist/cli/errors.d.ts +9 -0
  24. package/dist/cli/errors.js +111 -0
  25. package/dist/cli.js +66 -463
  26. package/dist/fsutil.js +1 -1
  27. package/dist/sync/attributes.d.ts +7 -0
  28. package/dist/sync/attributes.js +90 -0
  29. package/dist/sync/conversations.d.ts +7 -0
  30. package/dist/sync/conversations.js +218 -0
  31. package/dist/sync/metadata.d.ts +8 -0
  32. package/dist/sync/metadata.js +124 -0
  33. package/dist/sync/projects.d.ts +13 -0
  34. package/dist/sync/projects.js +283 -0
  35. package/dist/sync/push.d.ts +7 -0
  36. package/dist/sync/push.js +171 -0
  37. package/dist/sync/skill-files.d.ts +42 -0
  38. package/dist/sync/skill-files.js +121 -0
  39. package/dist/sync/status.d.ts +6 -0
  40. package/dist/sync/status.js +247 -0
  41. package/dist/sync.d.ts +10 -8
  42. package/dist/sync.js +12 -1197
  43. package/dist/types.d.ts +0 -1
  44. package/package.json +2 -2
  45. package/src/cli/commands/conversations.ts +47 -0
  46. package/src/cli/commands/help.ts +50 -0
  47. package/src/cli/commands/import-akb.ts +71 -0
  48. package/src/cli/commands/list-customers.ts +14 -0
  49. package/src/cli/commands/meta.ts +26 -0
  50. package/src/cli/commands/pull-attributes.ts +23 -0
  51. package/src/cli/commands/pull.ts +43 -0
  52. package/src/cli/commands/push.ts +47 -0
  53. package/src/cli/commands/status.ts +30 -0
  54. package/src/cli/customer-selection.ts +135 -0
  55. package/src/cli/errors.ts +111 -0
  56. package/src/cli.ts +77 -471
  57. package/src/fsutil.ts +1 -1
  58. package/src/sync/attributes.ts +110 -0
  59. package/src/sync/conversations.ts +257 -0
  60. package/src/sync/metadata.ts +153 -0
  61. package/src/sync/projects.ts +359 -0
  62. package/src/sync/push.ts +200 -0
  63. package/src/sync/skill-files.ts +176 -0
  64. package/src/sync/status.ts +277 -0
  65. package/src/sync.ts +14 -1389
  66. package/src/types.ts +0 -1
package/src/sync.ts CHANGED
@@ -1,1389 +1,14 @@
1
- import {
2
- listProjects,
3
- listAgents,
4
- listFlowSkills,
5
- updateSkill,
6
- listFlowEvents,
7
- listFlowStates,
8
- getProjectMeta,
9
- getCustomerAttributes,
10
- updateCustomerAttribute,
11
- listUserPersonas,
12
- getChatHistory
13
- } from './api.js';
14
- import {
15
- ensureState,
16
- skillPath,
17
- skillScriptPath,
18
- writeFileSafe,
19
- readIfExists,
20
- mapPath,
21
- projectMetadataPath,
22
- agentMetadataPath,
23
- flowMetadataPath,
24
- skillMetadataPath,
25
- flowsYamlPath,
26
- customerAttributesPath,
27
- customerAttributesMapPath,
28
- customerAttributesBackupPath
29
- } from './fsutil.js';
30
- import fs from 'fs-extra';
31
- import { sha256, loadHashes, saveHashes } from './hash.js';
32
- import yaml from 'js-yaml';
33
- import pLimit from 'p-limit';
34
- import type { AxiosInstance } from 'axios';
35
- import type {
36
- Agent,
37
- ProjectData,
38
- ProjectMap,
39
- LegacyProjectMap,
40
- HashStore,
41
- FlowsYamlData,
42
- FlowsYamlAgent,
43
- FlowsYamlFlow,
44
- FlowsYamlSkill,
45
- FlowsYamlEvent,
46
- FlowsYamlState,
47
- CustomerConfig,
48
- ProjectMetadata,
49
- AgentMetadata,
50
- FlowMetadata,
51
- SkillMetadata,
52
- FlowEvent,
53
- FlowState,
54
- CustomerAttribute,
55
- UserPersona,
56
- ConversationAct,
57
- ConversationOptions,
58
- ConversationsData,
59
- ProcessedPersona,
60
- ProcessedAct
61
- } from './types.js';
62
-
63
- // Concurrency limits for API operations
64
- const concurrencyLimit = pLimit(5);
65
-
66
- // Type guards for better type safety
67
- function isProjectMap(x: unknown): x is ProjectMap {
68
- return !!x && typeof x === 'object' && 'projects' in x;
69
- }
70
-
71
- function isLegacyProjectMap(x: unknown): x is LegacyProjectMap {
72
- return !!x && typeof x === 'object' && 'agents' in x;
73
- }
74
-
75
- export async function saveCustomerAttributes(
76
- client: AxiosInstance,
77
- customer: CustomerConfig,
78
- verbose: boolean = false
79
- ): Promise<void> {
80
- if (verbose) console.log(`🔍 Fetching customer attributes for ${customer.idn}...`);
81
-
82
- try {
83
- const response = await getCustomerAttributes(client, true); // Include hidden attributes
84
-
85
- // API returns { groups: [...], attributes: [...] }
86
- // We only want the attributes array in the expected format
87
- const attributes = response.attributes || response;
88
- if (verbose) console.log(`📦 Found ${Array.isArray(attributes) ? attributes.length : 'invalid'} attributes`);
89
-
90
- // Create ID mapping for push operations (separate from YAML)
91
- const idMapping: Record<string, string> = {};
92
-
93
- // Transform attributes to match reference format exactly (no ID fields)
94
- const cleanAttributes = Array.isArray(attributes) ? attributes.map(attr => {
95
- // Store ID mapping for push operations
96
- if (attr.id) {
97
- idMapping[attr.idn] = attr.id;
98
- }
99
-
100
- // Special handling for complex JSON string values
101
- let processedValue = attr.value;
102
- if (typeof attr.value === 'string' && attr.value.startsWith('[{') && attr.value.endsWith('}]')) {
103
- try {
104
- // Parse and reformat JSON for better readability
105
- const parsed = JSON.parse(attr.value);
106
- processedValue = JSON.stringify(parsed, null, 0); // No extra spacing, but valid JSON
107
- } catch (e) {
108
- // Keep original if parsing fails
109
- processedValue = attr.value;
110
- }
111
- }
112
-
113
- const cleanAttr: any = {
114
- idn: attr.idn,
115
- value: processedValue,
116
- title: attr.title || "",
117
- description: attr.description || "",
118
- group: attr.group || "",
119
- is_hidden: attr.is_hidden,
120
- possible_values: attr.possible_values || [],
121
- value_type: `__ENUM_PLACEHOLDER_${attr.value_type}__`
122
- };
123
- return cleanAttr;
124
- }) : [];
125
-
126
- const attributesYaml = {
127
- attributes: cleanAttributes
128
- };
129
-
130
- // Configure YAML output to match reference format exactly
131
- let yamlContent = yaml.dump(attributesYaml, {
132
- indent: 2,
133
- quotingType: '"',
134
- forceQuotes: false,
135
- lineWidth: 80, // Wrap long lines to match reference format
136
- noRefs: true,
137
- sortKeys: false,
138
- flowLevel: -1, // Never use flow syntax
139
- styles: {
140
- '!!str': 'folded' // Use folded style for better line wrapping of long strings
141
- }
142
- });
143
-
144
- // Post-process to fix enum format and improve JSON string formatting
145
- yamlContent = yamlContent.replace(/__ENUM_PLACEHOLDER_(\w+)__/g, '!enum "AttributeValueTypes.$1"');
146
-
147
- // Fix JSON string formatting to match reference (remove escape characters)
148
- yamlContent = yamlContent.replace(/\\"/g, '"');
149
-
150
- // Save all files: attributes.yaml, ID mapping, and backup for diff tracking
151
- await writeFileSafe(customerAttributesPath(customer.idn), yamlContent);
152
- await writeFileSafe(customerAttributesMapPath(customer.idn), JSON.stringify(idMapping, null, 2));
153
- await writeFileSafe(customerAttributesBackupPath(customer.idn), yamlContent);
154
-
155
- if (verbose) {
156
- console.log(`✓ Saved customer attributes to ${customerAttributesPath(customer.idn)}`);
157
- console.log(`✓ Saved attribute ID mapping to ${customerAttributesMapPath(customer.idn)}`);
158
- console.log(`✓ Created attributes backup for diff tracking`);
159
- }
160
- } catch (error) {
161
- console.error(`❌ Failed to save customer attributes for ${customer.idn}:`, error);
162
- throw error;
163
- }
164
- }
165
-
166
- export async function pullSingleProject(
167
- client: AxiosInstance,
168
- customer: CustomerConfig,
169
- projectId: string,
170
- projectIdn: string,
171
- verbose: boolean = false
172
- ): Promise<ProjectData> {
173
- if (verbose) console.log(`🔍 Fetching agents for project ${projectId} (${projectIdn}) for customer ${customer.idn}...`);
174
- const agents = await listAgents(client, projectId);
175
- if (verbose) console.log(`📦 Found ${agents.length} agents`);
176
-
177
- // Get and create project metadata
178
- const projectMeta = await getProjectMeta(client, projectId);
179
- const projectMetadata: ProjectMetadata = {
180
- id: projectMeta.id,
181
- idn: projectMeta.idn,
182
- title: projectMeta.title,
183
- ...(projectMeta.description && { description: projectMeta.description }),
184
- ...(projectMeta.created_at && { created_at: projectMeta.created_at }),
185
- ...(projectMeta.updated_at && { updated_at: projectMeta.updated_at })
186
- };
187
- await writeFileSafe(projectMetadataPath(customer.idn, projectIdn), yaml.dump(projectMetadata, { indent: 2 }));
188
- if (verbose) console.log(`✓ Created project metadata.yaml for ${projectIdn}`);
189
-
190
- // Legacy metadata.json generation removed - YAML is sufficient
191
-
192
- const projectMap: ProjectData = { projectId, projectIdn, agents: {} };
193
-
194
- for (const agent of agents) {
195
- const aKey = agent.idn;
196
- projectMap.agents[aKey] = { id: agent.id, flows: {} };
197
-
198
- // Create agent metadata
199
- const agentMetadata: AgentMetadata = {
200
- id: agent.id,
201
- idn: agent.idn,
202
- ...(agent.title && { title: agent.title }),
203
- ...(agent.description && { description: agent.description })
204
- };
205
- await writeFileSafe(agentMetadataPath(customer.idn, projectIdn, agent.idn), yaml.dump(agentMetadata, { indent: 2 }));
206
- if (verbose) console.log(` ✓ Created agent metadata for ${agent.idn}`);
207
-
208
- for (const flow of agent.flows ?? []) {
209
- projectMap.agents[aKey]!.flows[flow.idn] = { id: flow.id, skills: {} };
210
-
211
- // Fetch flow events and state fields for metadata
212
- let flowEvents: FlowEvent[] = [];
213
- let flowStates: FlowState[] = [];
214
-
215
- try {
216
- flowEvents = await listFlowEvents(client, flow.id);
217
- if (verbose) console.log(` 📋 Found ${flowEvents.length} events for flow ${flow.idn}`);
218
- } catch (error) {
219
- if (verbose) console.log(` ⚠️ No events found for flow ${flow.idn}`);
220
- }
221
-
222
- try {
223
- flowStates = await listFlowStates(client, flow.id);
224
- if (verbose) console.log(` 📊 Found ${flowStates.length} state fields for flow ${flow.idn}`);
225
- } catch (error) {
226
- if (verbose) console.log(` ⚠️ No state fields found for flow ${flow.idn}`);
227
- }
228
-
229
- // Create flow metadata
230
- const flowMetadata: FlowMetadata = {
231
- id: flow.id,
232
- idn: flow.idn,
233
- title: flow.title,
234
- ...(flow.description && { description: flow.description }),
235
- default_runner_type: flow.default_runner_type,
236
- default_model: flow.default_model,
237
- events: flowEvents,
238
- state_fields: flowStates
239
- };
240
- await writeFileSafe(flowMetadataPath(customer.idn, projectIdn, agent.idn, flow.idn), yaml.dump(flowMetadata, { indent: 2 }));
241
- if (verbose) console.log(` ✓ Created flow metadata for ${flow.idn}`);
242
-
243
- const skills = await listFlowSkills(client, flow.id);
244
-
245
- // Process skills concurrently with limited concurrency
246
- await Promise.all(skills.map(skill => concurrencyLimit(async () => {
247
- // Create skill folder and script file
248
- const scriptFile = skillScriptPath(customer.idn, projectIdn, agent.idn, flow.idn, skill.idn, skill.runner_type);
249
- await writeFileSafe(scriptFile, skill.prompt_script || '');
250
-
251
- // Create skill metadata
252
- const skillMetadata: SkillMetadata = {
253
- id: skill.id,
254
- idn: skill.idn,
255
- title: skill.title,
256
- runner_type: skill.runner_type,
257
- model: skill.model,
258
- parameters: [...skill.parameters],
259
- path: skill.path || undefined
260
- };
261
- const skillMetaFile = skillMetadataPath(customer.idn, projectIdn, agent.idn, flow.idn, skill.idn);
262
- await writeFileSafe(skillMetaFile, yaml.dump(skillMetadata, { indent: 2 }));
263
-
264
- // Store complete skill metadata for push operations (keep for backwards compatibility)
265
- projectMap.agents[aKey]!.flows[flow.idn]!.skills[skill.idn] = {
266
- id: skill.id,
267
- title: skill.title,
268
- idn: skill.idn,
269
- runner_type: skill.runner_type,
270
- model: skill.model,
271
- parameters: [...skill.parameters],
272
- path: skill.path || undefined
273
- };
274
- console.log(`✓ Created skill folder and metadata for ${skill.idn}`);
275
- })));
276
- }
277
- }
278
-
279
- // Generate flows.yaml for this project (backwards compatibility)
280
- if (verbose) console.log(`📄 Generating flows.yaml...`);
281
- await generateFlowsYaml(client, customer, agents, verbose);
282
-
283
- return projectMap;
284
- }
285
-
286
- export async function pullAll(
287
- client: AxiosInstance,
288
- customer: CustomerConfig,
289
- projectId: string | null = null,
290
- verbose: boolean = false
291
- ): Promise<void> {
292
- await ensureState(customer.idn);
293
-
294
- if (projectId) {
295
- // Single project mode
296
- const projectMeta = await getProjectMeta(client, projectId);
297
- const projectMap = await pullSingleProject(client, customer, projectId, projectMeta.idn, verbose);
298
-
299
- const idMap: ProjectMap = { projects: { [projectMeta.idn]: projectMap } };
300
- await fs.writeJson(mapPath(customer.idn), idMap, { spaces: 2 });
301
-
302
- // Generate hash tracking for this project (both legacy and new paths)
303
- const hashes: HashStore = {};
304
- for (const [agentIdn, agentObj] of Object.entries(projectMap.agents)) {
305
- for (const [flowIdn, flowObj] of Object.entries(agentObj.flows)) {
306
- for (const [skillIdn, skillMeta] of Object.entries(flowObj.skills)) {
307
- // Track new skill script path
308
- const newPath = skillScriptPath(customer.idn, projectMeta.idn, agentIdn, flowIdn, skillIdn, skillMeta.runner_type);
309
- const content = await fs.readFile(newPath, 'utf8');
310
- hashes[newPath] = sha256(content);
311
-
312
- // Track skill metadata.yaml file
313
- const metadataPath = skillMetadataPath(customer.idn, projectMeta.idn, agentIdn, flowIdn, skillIdn);
314
- if (await fs.pathExists(metadataPath)) {
315
- const metadataContent = await fs.readFile(metadataPath, 'utf8');
316
- hashes[metadataPath] = sha256(metadataContent);
317
- }
318
-
319
- // Also track legacy path for backwards compatibility during transition
320
- const legacyPath = skillPath(customer.idn, projectMeta.idn, agentIdn, flowIdn, skillIdn, skillMeta.runner_type);
321
- hashes[legacyPath] = sha256(content);
322
- }
323
- }
324
- }
325
-
326
- // Save customer attributes before hash tracking
327
- try {
328
- await saveCustomerAttributes(client, customer, verbose);
329
-
330
- // Add attributes.yaml to hash tracking
331
- const attributesFile = customerAttributesPath(customer.idn);
332
- if (await fs.pathExists(attributesFile)) {
333
- const attributesContent = await fs.readFile(attributesFile, 'utf8');
334
- hashes[attributesFile] = sha256(attributesContent);
335
- if (verbose) console.log(`✓ Added attributes.yaml to hash tracking`);
336
- }
337
-
338
- // Add flows.yaml to hash tracking
339
- const flowsFile = flowsYamlPath(customer.idn);
340
- if (await fs.pathExists(flowsFile)) {
341
- const flowsContent = await fs.readFile(flowsFile, 'utf8');
342
- hashes[flowsFile] = sha256(flowsContent);
343
- if (verbose) console.log(`✓ Added flows.yaml to hash tracking`);
344
- }
345
- } catch (error) {
346
- console.error(`❌ Failed to save customer attributes for ${customer.idn}:`, error);
347
- // Don't throw - continue with the rest of the process
348
- }
349
-
350
- await saveHashes(hashes, customer.idn);
351
- return;
352
- }
353
-
354
- // Multi-project mode
355
- if (verbose) console.log(`🔍 Fetching all projects for customer ${customer.idn}...`);
356
- const projects = await listProjects(client);
357
- if (verbose) console.log(`📦 Found ${projects.length} projects`);
358
-
359
- const idMap: ProjectMap = { projects: {} };
360
- const allHashes: HashStore = {};
361
-
362
- for (const project of projects) {
363
- if (verbose) console.log(`\n📁 Processing project: ${project.idn} (${project.title})`);
364
- const projectMap = await pullSingleProject(client, customer, project.id, project.idn, verbose);
365
- idMap.projects[project.idn] = projectMap;
366
-
367
- // Collect hashes for this project (both legacy and new paths)
368
- for (const [agentIdn, agentObj] of Object.entries(projectMap.agents)) {
369
- for (const [flowIdn, flowObj] of Object.entries(agentObj.flows)) {
370
- for (const [skillIdn, skillMeta] of Object.entries(flowObj.skills)) {
371
- // Track new skill script path
372
- const newPath = skillScriptPath(customer.idn, project.idn, agentIdn, flowIdn, skillIdn, skillMeta.runner_type);
373
- const content = await fs.readFile(newPath, 'utf8');
374
- allHashes[newPath] = sha256(content);
375
-
376
- // Track skill metadata.yaml file
377
- const metadataPath = skillMetadataPath(customer.idn, project.idn, agentIdn, flowIdn, skillIdn);
378
- if (await fs.pathExists(metadataPath)) {
379
- const metadataContent = await fs.readFile(metadataPath, 'utf8');
380
- allHashes[metadataPath] = sha256(metadataContent);
381
- }
382
-
383
- // Also track legacy path for backwards compatibility during transition
384
- const legacyPath = skillPath(customer.idn, project.idn, agentIdn, flowIdn, skillIdn, skillMeta.runner_type);
385
- allHashes[legacyPath] = sha256(content);
386
- }
387
- }
388
- }
389
- }
390
-
391
- await fs.writeJson(mapPath(customer.idn), idMap, { spaces: 2 });
392
-
393
- // Save customer attributes before hash tracking
394
- try {
395
- await saveCustomerAttributes(client, customer, verbose);
396
-
397
- // Add attributes.yaml to hash tracking
398
- const attributesFile = customerAttributesPath(customer.idn);
399
- if (await fs.pathExists(attributesFile)) {
400
- const attributesContent = await fs.readFile(attributesFile, 'utf8');
401
- allHashes[attributesFile] = sha256(attributesContent);
402
- if (verbose) console.log(`✓ Added attributes.yaml to hash tracking`);
403
- }
404
-
405
- // Add flows.yaml to hash tracking
406
- const flowsFile = flowsYamlPath(customer.idn);
407
- if (await fs.pathExists(flowsFile)) {
408
- const flowsContent = await fs.readFile(flowsFile, 'utf8');
409
- allHashes[flowsFile] = sha256(flowsContent);
410
- if (verbose) console.log(`✓ Added flows.yaml to hash tracking`);
411
- }
412
- } catch (error) {
413
- console.error(`❌ Failed to save customer attributes for ${customer.idn}:`, error);
414
- // Don't throw - continue with the rest of the process
415
- }
416
-
417
- await saveHashes(allHashes, customer.idn);
418
- }
419
-
420
- export async function pushChanged(client: AxiosInstance, customer: CustomerConfig, verbose: boolean = false): Promise<void> {
421
- await ensureState(customer.idn);
422
- if (!(await fs.pathExists(mapPath(customer.idn)))) {
423
- throw new Error(`Missing .newo/${customer.idn}/map.json. Run \`newo pull --customer ${customer.idn}\` first.`);
424
- }
425
-
426
- if (verbose) console.log(`📋 Loading project mapping for customer ${customer.idn}...`);
427
- const idMapData = await fs.readJson(mapPath(customer.idn)) as unknown;
428
- if (verbose) console.log('🔍 Loading file hashes...');
429
- const oldHashes = await loadHashes(customer.idn);
430
- const newHashes: HashStore = { ...oldHashes };
431
-
432
- if (verbose) console.log('🔄 Scanning for changes...');
433
- let pushed = 0;
434
- let scanned = 0;
435
- let metadataChanged = false;
436
-
437
- // Handle both old single-project format and new multi-project format with type guards
438
- const projects = isProjectMap(idMapData) && idMapData.projects
439
- ? idMapData.projects
440
- : isLegacyProjectMap(idMapData)
441
- ? { '': idMapData as ProjectData }
442
- : (() => { throw new Error('Invalid project map format'); })();
443
-
444
- for (const [projectIdn, projectData] of Object.entries(projects)) {
445
- if (verbose && projectIdn) console.log(`📁 Scanning project: ${projectIdn}`);
446
-
447
- for (const [agentIdn, agentObj] of Object.entries(projectData.agents)) {
448
- if (verbose) console.log(` 📁 Scanning agent: ${agentIdn}`);
449
- for (const [flowIdn, flowObj] of Object.entries(agentObj.flows)) {
450
- if (verbose) console.log(` 📁 Scanning flow: ${flowIdn}`);
451
- for (const [skillIdn, skillMeta] of Object.entries(flowObj.skills)) {
452
- // Try new folder structure first
453
- const newPath = projectIdn ?
454
- skillScriptPath(customer.idn, projectIdn, agentIdn, flowIdn, skillIdn, skillMeta.runner_type) :
455
- skillScriptPath(customer.idn, '', agentIdn, flowIdn, skillIdn, skillMeta.runner_type);
456
-
457
- // Fallback to legacy structure
458
- const legacyPath = projectIdn ?
459
- skillPath(customer.idn, projectIdn, agentIdn, flowIdn, skillIdn, skillMeta.runner_type) :
460
- skillPath(customer.idn, '', agentIdn, flowIdn, skillIdn, skillMeta.runner_type);
461
-
462
- let currentPath = newPath;
463
- let content = await readIfExists(newPath);
464
-
465
- // If new structure doesn't exist, try legacy structure
466
- if (content === null) {
467
- content = await readIfExists(legacyPath);
468
- currentPath = legacyPath;
469
- }
470
-
471
- scanned++;
472
- if (verbose) console.log(` 📄 Checking: ${currentPath}`);
473
-
474
- if (content === null) {
475
- if (verbose) console.log(` ⚠️ File not found: ${currentPath}`);
476
- continue;
477
- }
478
-
479
- const h = sha256(content);
480
- const oldHash = oldHashes[currentPath];
481
- if (verbose) {
482
- console.log(` 🔍 Hash comparison:`);
483
- console.log(` Old: ${oldHash || 'none'}`);
484
- console.log(` New: ${h}`);
485
- }
486
-
487
- if (oldHash !== h) {
488
- if (verbose) console.log(` 🔄 File changed, preparing to push...`);
489
-
490
- // For new folder structure, try to load metadata from YAML file
491
- let skillMetadata = skillMeta;
492
- if (currentPath === newPath) {
493
- const metadataFile = projectIdn ?
494
- skillMetadataPath(customer.idn, projectIdn, agentIdn, flowIdn, skillIdn) :
495
- skillMetadataPath(customer.idn, '', agentIdn, flowIdn, skillIdn);
496
-
497
- const metadataContent = await readIfExists(metadataFile);
498
- if (metadataContent) {
499
- try {
500
- const yamlMetadata = yaml.load(metadataContent) as SkillMetadata;
501
- skillMetadata = yamlMetadata;
502
- if (verbose) console.log(` 📄 Loaded skill metadata from ${metadataFile}`);
503
- } catch (error) {
504
- if (verbose) console.log(` ⚠️ Failed to parse skill metadata, using project map data`);
505
- }
506
- }
507
- }
508
-
509
- // Create complete skill object with updated prompt_script
510
- const skillObject = {
511
- id: skillMetadata.id,
512
- title: skillMetadata.title,
513
- idn: skillMetadata.idn,
514
- prompt_script: content,
515
- runner_type: skillMetadata.runner_type,
516
- model: skillMetadata.model,
517
- parameters: skillMetadata.parameters,
518
- path: skillMetadata.path || undefined
519
- };
520
-
521
- if (verbose) {
522
- console.log(` 📤 Pushing skill object:`);
523
- console.log(` ID: ${skillObject.id}`);
524
- console.log(` Title: ${skillObject.title}`);
525
- console.log(` IDN: ${skillObject.idn}`);
526
- console.log(` Content length: ${content.length} chars`);
527
- console.log(` Content preview: ${content.substring(0, 100).replace(/\n/g, '\\n')}...`);
528
- }
529
-
530
- await updateSkill(client, skillObject);
531
- console.log(`↑ Pushed ${currentPath}`);
532
- newHashes[currentPath] = h;
533
- pushed++;
534
- } else if (verbose) {
535
- console.log(` ✓ No changes`);
536
- }
537
- }
538
- }
539
- }
540
- }
541
-
542
- // Check for metadata-only changes (when metadata changed but script didn't)
543
- try {
544
- for (const [projectIdn, projectData] of Object.entries(projects)) {
545
- for (const [agentIdn, agentObj] of Object.entries(projectData.agents)) {
546
- for (const [flowIdn, flowObj] of Object.entries(agentObj.flows)) {
547
- for (const [skillIdn, skillMeta] of Object.entries(flowObj.skills)) {
548
- const metadataPath = projectIdn ?
549
- skillMetadataPath(customer.idn, projectIdn, agentIdn, flowIdn, skillIdn) :
550
- skillMetadataPath(customer.idn, '', agentIdn, flowIdn, skillIdn);
551
-
552
- if (await fs.pathExists(metadataPath)) {
553
- const metadataContent = await fs.readFile(metadataPath, 'utf8');
554
- const h = sha256(metadataContent);
555
- const oldHash = oldHashes[metadataPath];
556
-
557
- if (oldHash !== h) {
558
- if (verbose) console.log(`🔄 Metadata-only change detected for ${skillIdn}, updating skill...`);
559
-
560
- try {
561
- // Load updated metadata
562
- const updatedMetadata = yaml.load(metadataContent) as SkillMetadata;
563
-
564
- // Get current script content
565
- const scriptPath = projectIdn ?
566
- skillScriptPath(customer.idn, projectIdn, agentIdn, flowIdn, skillIdn, skillMeta.runner_type) :
567
- skillScriptPath(customer.idn, '', agentIdn, flowIdn, skillIdn, skillMeta.runner_type);
568
-
569
- let scriptContent = '';
570
- if (await fs.pathExists(scriptPath)) {
571
- scriptContent = await fs.readFile(scriptPath, 'utf8');
572
- }
573
-
574
- // Create skill object with updated metadata
575
- const skillObject = {
576
- id: updatedMetadata.id,
577
- title: updatedMetadata.title,
578
- idn: updatedMetadata.idn,
579
- prompt_script: scriptContent,
580
- runner_type: updatedMetadata.runner_type,
581
- model: updatedMetadata.model,
582
- parameters: updatedMetadata.parameters,
583
- path: updatedMetadata.path || undefined
584
- };
585
-
586
- await updateSkill(client, skillObject);
587
- console.log(`↑ Pushed metadata update for skill: ${skillIdn} (${updatedMetadata.title})`);
588
-
589
- newHashes[metadataPath] = h;
590
- pushed++;
591
- metadataChanged = true;
592
-
593
- } catch (error) {
594
- console.error(`❌ Failed to push metadata for ${skillIdn}: ${error instanceof Error ? error.message : String(error)}`);
595
- }
596
- }
597
- }
598
- }
599
- }
600
- }
601
- }
602
- } catch (error) {
603
- if (verbose) console.log(`⚠️ Metadata push check failed: ${error instanceof Error ? error.message : String(error)}`);
604
- }
605
-
606
- if (verbose) console.log(`🔄 Scanned ${scanned} files, found ${pushed} changes`);
607
-
608
- // Check for attributes changes and push specific changed attributes only
609
- try {
610
- const attributesFile = customerAttributesPath(customer.idn);
611
- const attributesMapFile = customerAttributesMapPath(customer.idn);
612
- const attributesBackupFile = customerAttributesBackupPath(customer.idn);
613
-
614
- if (await fs.pathExists(attributesFile) && await fs.pathExists(attributesMapFile)) {
615
- if (verbose) console.log('🔍 Checking customer attributes for changes...');
616
-
617
- const currentContent = await fs.readFile(attributesFile, 'utf8');
618
-
619
- // Check if backup exists for diff comparison
620
- if (await fs.pathExists(attributesBackupFile)) {
621
- const backupContent = await fs.readFile(attributesBackupFile, 'utf8');
622
-
623
- if (currentContent !== backupContent) {
624
- if (verbose) console.log(`🔄 Attributes file changed, analyzing differences...`);
625
-
626
- try {
627
- // Load ID mapping for push operations
628
- const idMapping = await fs.readJson(attributesMapFile) as Record<string, string>;
629
-
630
- // Parse both versions to find changed attributes
631
- const parseYaml = (content: string) => {
632
- let yamlContent = content.replace(/!enum "([^"]+)"/g, '"$1"');
633
- return yaml.load(yamlContent) as { attributes: any[] };
634
- };
635
-
636
- const currentData = parseYaml(currentContent);
637
- const backupData = parseYaml(backupContent);
638
-
639
- if (currentData?.attributes && backupData?.attributes) {
640
- // Create maps for comparison
641
- const currentAttrs = new Map(currentData.attributes.map(attr => [attr.idn, attr]));
642
- const backupAttrs = new Map(backupData.attributes.map(attr => [attr.idn, attr]));
643
-
644
- let attributesPushed = 0;
645
-
646
- // Find changed attributes
647
- for (const [idn, currentAttr] of currentAttrs) {
648
- const backupAttr = backupAttrs.get(idn);
649
-
650
- // Check if attribute changed (deep comparison of key fields)
651
- const hasChanged = !backupAttr ||
652
- currentAttr.value !== backupAttr.value ||
653
- currentAttr.title !== backupAttr.title ||
654
- currentAttr.description !== backupAttr.description ||
655
- currentAttr.group !== backupAttr.group ||
656
- currentAttr.is_hidden !== backupAttr.is_hidden;
657
-
658
- if (hasChanged) {
659
- const attributeId = idMapping[idn];
660
- if (!attributeId) {
661
- if (verbose) console.log(`⚠️ Skipping ${idn} - no ID mapping`);
662
- continue;
663
- }
664
-
665
- // Create attribute object for push
666
- const attributeToUpdate: CustomerAttribute = {
667
- id: attributeId,
668
- idn: currentAttr.idn,
669
- value: currentAttr.value,
670
- title: currentAttr.title || "",
671
- description: currentAttr.description || "",
672
- group: currentAttr.group || "",
673
- is_hidden: currentAttr.is_hidden,
674
- possible_values: currentAttr.possible_values || [],
675
- value_type: currentAttr.value_type?.replace(/^"?AttributeValueTypes\.(.+)"?$/, '$1') || "string"
676
- };
677
-
678
- await updateCustomerAttribute(client, attributeToUpdate);
679
- attributesPushed++;
680
-
681
- if (verbose) {
682
- console.log(` ✓ Pushed changed attribute: ${idn}`);
683
- console.log(` Old value: ${backupAttr?.value || 'N/A'}`);
684
- console.log(` New value: ${currentAttr.value}`);
685
- }
686
- }
687
- }
688
-
689
- if (attributesPushed > 0) {
690
- console.log(`↑ Pushed ${attributesPushed} changed customer attributes to NEWO API`);
691
-
692
- // Show summary of what was pushed
693
- console.log(` 📊 Pushed attributes:`);
694
- for (const [idn, currentAttr] of currentAttrs) {
695
- const backupAttr = backupAttrs.get(idn);
696
- const hasChanged = !backupAttr ||
697
- currentAttr.value !== backupAttr.value ||
698
- currentAttr.title !== backupAttr.title ||
699
- currentAttr.description !== backupAttr.description ||
700
- currentAttr.group !== backupAttr.group ||
701
- currentAttr.is_hidden !== backupAttr.is_hidden;
702
-
703
- if (hasChanged) {
704
- console.log(` • ${idn}: ${currentAttr.title || 'No title'}`);
705
- console.log(` Value: ${currentAttr.value}`);
706
- }
707
- }
708
-
709
- // Update backup file after successful push
710
- await fs.writeFile(attributesBackupFile, currentContent, 'utf8');
711
-
712
- newHashes[attributesFile] = sha256(currentContent);
713
- pushed++;
714
- } else if (verbose) {
715
- console.log(` ✓ No attribute value changes detected`);
716
- }
717
-
718
- } else {
719
- console.log(`⚠️ Failed to parse attributes for comparison`);
720
- }
721
-
722
- } catch (error) {
723
- console.error(`❌ Failed to push changed attributes: ${error instanceof Error ? error.message : String(error)}`);
724
- // Don't update hash/backup on failure so it will retry next time
725
- }
726
- } else if (verbose) {
727
- console.log(` ✓ No attributes file changes`);
728
- }
729
- } else {
730
- // No backup exists, create initial backup
731
- await fs.writeFile(attributesBackupFile, currentContent, 'utf8');
732
- if (verbose) console.log(`✓ Created initial attributes backup for diff tracking`);
733
- }
734
- } else if (verbose) {
735
- console.log('ℹ️ No attributes file or ID mapping found for push checking');
736
- }
737
- } catch (error) {
738
- if (verbose) console.log(`⚠️ Attributes push check failed: ${error instanceof Error ? error.message : String(error)}`);
739
- }
740
-
741
- // Regenerate flows.yaml if metadata changed
742
- if (metadataChanged) {
743
- try {
744
- if (verbose) console.log('🔄 Metadata changed, regenerating flows.yaml...');
745
-
746
- // Create backup of current flows.yaml for format comparison
747
- const flowsFile = flowsYamlPath(customer.idn);
748
- let flowsBackup = '';
749
- if (await fs.pathExists(flowsFile)) {
750
- flowsBackup = await fs.readFile(flowsFile, 'utf8');
751
- const backupPath = `${flowsFile}.backup`;
752
- await fs.writeFile(backupPath, flowsBackup, 'utf8');
753
- if (verbose) console.log(`✓ Created flows.yaml backup at ${backupPath}`);
754
- }
755
-
756
- // Re-fetch agents for flows.yaml regeneration
757
- const agentsForFlows: Agent[] = [];
758
- for (const projectData of Object.values(projects)) {
759
- const projectAgents = await listAgents(client, projectData.projectId);
760
- agentsForFlows.push(...projectAgents);
761
- }
762
-
763
- // Regenerate flows.yaml
764
- await generateFlowsYaml(client, customer, agentsForFlows, verbose);
765
-
766
- // Update flows.yaml hash
767
- if (await fs.pathExists(flowsFile)) {
768
- const newFlowsContent = await fs.readFile(flowsFile, 'utf8');
769
- newHashes[flowsFile] = sha256(newFlowsContent);
770
-
771
- // Compare format with backup
772
- if (flowsBackup) {
773
- const sizeDiff = newFlowsContent.length - flowsBackup.length;
774
- if (verbose) {
775
- console.log(`✓ Regenerated flows.yaml (size change: ${sizeDiff > 0 ? '+' : ''}${sizeDiff} chars)`);
776
- }
777
- }
778
- }
779
-
780
- console.log('↑ Regenerated flows.yaml due to metadata changes');
781
-
782
- } catch (error) {
783
- console.error(`❌ Failed to regenerate flows.yaml: ${error instanceof Error ? error.message : String(error)}`);
784
- }
785
- }
786
-
787
- await saveHashes(newHashes, customer.idn);
788
- console.log(pushed ? `✅ Push complete. ${pushed} file(s) updated.` : '✅ Nothing to push.');
789
- }
790
-
791
- export async function status(customer: CustomerConfig, verbose: boolean = false): Promise<void> {
792
- await ensureState(customer.idn);
793
- if (!(await fs.pathExists(mapPath(customer.idn)))) {
794
- console.log(`No map for customer ${customer.idn}. Run \`newo pull --customer ${customer.idn}\` first.`);
795
- return;
796
- }
797
-
798
- if (verbose) console.log(`📋 Loading project mapping and hashes for customer ${customer.idn}...`);
799
- const idMapData = await fs.readJson(mapPath(customer.idn)) as unknown;
800
- const hashes = await loadHashes(customer.idn);
801
- let dirty = 0;
802
-
803
- // Handle both old single-project format and new multi-project format with type guards
804
- const projects = isProjectMap(idMapData) && idMapData.projects
805
- ? idMapData.projects
806
- : isLegacyProjectMap(idMapData)
807
- ? { '': idMapData as ProjectData }
808
- : (() => { throw new Error('Invalid project map format'); })();
809
-
810
- for (const [projectIdn, projectData] of Object.entries(projects)) {
811
- if (verbose && projectIdn) console.log(`📁 Checking project: ${projectIdn}`);
812
-
813
- for (const [agentIdn, agentObj] of Object.entries(projectData.agents)) {
814
- if (verbose) console.log(` 📁 Checking agent: ${agentIdn}`);
815
- for (const [flowIdn, flowObj] of Object.entries(agentObj.flows)) {
816
- if (verbose) console.log(` 📁 Checking flow: ${flowIdn}`);
817
- for (const [skillIdn, skillMeta] of Object.entries(flowObj.skills)) {
818
- // Try new folder structure first
819
- const newPath = projectIdn ?
820
- skillScriptPath(customer.idn, projectIdn, agentIdn, flowIdn, skillIdn, skillMeta.runner_type) :
821
- skillScriptPath(customer.idn, '', agentIdn, flowIdn, skillIdn, skillMeta.runner_type);
822
-
823
- // Fallback to legacy structure
824
- const legacyPath = projectIdn ?
825
- skillPath(customer.idn, projectIdn, agentIdn, flowIdn, skillIdn, skillMeta.runner_type) :
826
- skillPath(customer.idn, '', agentIdn, flowIdn, skillIdn, skillMeta.runner_type);
827
-
828
- let currentPath = newPath;
829
- let exists = await fs.pathExists(newPath);
830
-
831
- // If new structure doesn't exist, try legacy structure
832
- if (!exists) {
833
- exists = await fs.pathExists(legacyPath);
834
- currentPath = legacyPath;
835
- }
836
-
837
- if (!exists) {
838
- console.log(`D ${currentPath}`);
839
- dirty++;
840
- if (verbose) console.log(` ❌ Deleted: ${currentPath}`);
841
- continue;
842
- }
843
-
844
- const content = await fs.readFile(currentPath, 'utf8');
845
- const h = sha256(content);
846
- const oldHash = hashes[currentPath];
847
-
848
- if (verbose) {
849
- console.log(` 📄 ${currentPath}`);
850
- console.log(` Old hash: ${oldHash || 'none'}`);
851
- console.log(` New hash: ${h}`);
852
- }
853
-
854
- if (oldHash !== h) {
855
- console.log(`M ${currentPath}`);
856
- dirty++;
857
- if (verbose) console.log(` 🔄 Modified: ${currentPath}`);
858
- } else if (verbose) {
859
- console.log(` ✓ Unchanged: ${currentPath}`);
860
- }
861
- }
862
-
863
- // Check metadata.yaml files for changes (after skill files)
864
- for (const [skillIdn] of Object.entries(flowObj.skills)) {
865
- const metadataPath = projectIdn ?
866
- skillMetadataPath(customer.idn, projectIdn, agentIdn, flowIdn, skillIdn) :
867
- skillMetadataPath(customer.idn, '', agentIdn, flowIdn, skillIdn);
868
-
869
- if (await fs.pathExists(metadataPath)) {
870
- const metadataContent = await fs.readFile(metadataPath, 'utf8');
871
- const h = sha256(metadataContent);
872
- const oldHash = hashes[metadataPath];
873
-
874
- if (verbose) {
875
- console.log(` 📄 ${metadataPath}`);
876
- console.log(` Old hash: ${oldHash || 'none'}`);
877
- console.log(` New hash: ${h}`);
878
- }
879
-
880
- if (oldHash !== h) {
881
- console.log(`M ${metadataPath}`);
882
- dirty++;
883
-
884
- // Show which metadata fields changed
885
- try {
886
- const newMetadata = yaml.load(metadataContent) as any;
887
-
888
- console.log(` 📊 Metadata changed for skill: ${skillIdn}`);
889
- if (newMetadata?.title) {
890
- console.log(` • Title: ${newMetadata.title}`);
891
- }
892
- if (newMetadata?.runner_type) {
893
- console.log(` • Runner: ${newMetadata.runner_type}`);
894
- }
895
- if (newMetadata?.model) {
896
- console.log(` • Model: ${newMetadata.model.provider_idn}/${newMetadata.model.model_idn}`);
897
- }
898
- } catch (e) {
899
- // Fallback to simple message
900
- if (verbose) console.log(` 🔄 Modified: metadata.yaml`);
901
- }
902
- } else if (verbose) {
903
- console.log(` ✓ Unchanged: ${metadataPath}`);
904
- }
905
- }
906
- }
907
- }
908
- }
909
- }
910
-
911
- // Check attributes file for changes
912
- try {
913
- const attributesFile = customerAttributesPath(customer.idn);
914
- if (await fs.pathExists(attributesFile)) {
915
- const content = await fs.readFile(attributesFile, 'utf8');
916
- const h = sha256(content);
917
- const oldHash = hashes[attributesFile];
918
-
919
- if (verbose) {
920
- console.log(`📄 ${attributesFile}`);
921
- console.log(` Old hash: ${oldHash || 'none'}`);
922
- console.log(` New hash: ${h}`);
923
- }
924
-
925
- if (oldHash !== h) {
926
- console.log(`M ${attributesFile}`);
927
- dirty++;
928
-
929
- // Show which attributes changed by comparing with backup
930
- try {
931
- const attributesBackupFile = customerAttributesBackupPath(customer.idn);
932
- if (await fs.pathExists(attributesBackupFile)) {
933
- const backupContent = await fs.readFile(attributesBackupFile, 'utf8');
934
-
935
- const parseYaml = (content: string) => {
936
- let yamlContent = content.replace(/!enum "([^"]+)"/g, '"$1"');
937
- return yaml.load(yamlContent) as { attributes: any[] };
938
- };
939
-
940
- const currentData = parseYaml(content);
941
- const backupData = parseYaml(backupContent);
942
-
943
- if (currentData?.attributes && backupData?.attributes) {
944
- const currentAttrs = new Map(currentData.attributes.map(attr => [attr.idn, attr]));
945
- const backupAttrs = new Map(backupData.attributes.map(attr => [attr.idn, attr]));
946
-
947
- const changedAttributes: string[] = [];
948
-
949
- for (const [idn, currentAttr] of currentAttrs) {
950
- const backupAttr = backupAttrs.get(idn);
951
- const hasChanged = !backupAttr ||
952
- currentAttr.value !== backupAttr.value ||
953
- currentAttr.title !== backupAttr.title ||
954
- currentAttr.description !== backupAttr.description ||
955
- currentAttr.group !== backupAttr.group ||
956
- currentAttr.is_hidden !== backupAttr.is_hidden;
957
-
958
- if (hasChanged) {
959
- changedAttributes.push(idn);
960
- }
961
- }
962
-
963
- if (changedAttributes.length > 0) {
964
- console.log(` 📊 Changed attributes (${changedAttributes.length}):`);
965
- changedAttributes.slice(0, 5).forEach(idn => {
966
- const current = currentAttrs.get(idn);
967
- const backup = backupAttrs.get(idn);
968
- console.log(` • ${idn}: ${current?.title || 'No title'}`);
969
- if (verbose) {
970
- console.log(` Old: ${backup?.value || 'N/A'}`);
971
- console.log(` New: ${current?.value || 'N/A'}`);
972
- }
973
- });
974
- if (changedAttributes.length > 5) {
975
- console.log(` ... and ${changedAttributes.length - 5} more`);
976
- }
977
- }
978
- }
979
- }
980
- } catch (e) {
981
- // Fallback to simple message if diff analysis fails
982
- }
983
-
984
- if (verbose) console.log(` 🔄 Modified: attributes.yaml`);
985
- } else if (verbose) {
986
- console.log(` ✓ Unchanged: attributes.yaml`);
987
- }
988
- }
989
- } catch (error) {
990
- if (verbose) console.log(`⚠️ Error checking attributes: ${error instanceof Error ? error.message : String(error)}`);
991
- }
992
-
993
- // Check flows.yaml file for changes
994
- const flowsFile = flowsYamlPath(customer.idn);
995
- if (await fs.pathExists(flowsFile)) {
996
- try {
997
- const flowsContent = await fs.readFile(flowsFile, 'utf8');
998
- const h = sha256(flowsContent);
999
- const oldHash = hashes[flowsFile];
1000
-
1001
- if (verbose) {
1002
- console.log(`📄 flows.yaml`);
1003
- console.log(` Old hash: ${oldHash || 'none'}`);
1004
- console.log(` New hash: ${h}`);
1005
- }
1006
-
1007
- if (oldHash !== h) {
1008
- console.log(`M ${flowsFile}`);
1009
- dirty++;
1010
- if (verbose) {
1011
- const flowsStats = await fs.stat(flowsFile);
1012
- console.log(` 🔄 Modified: flows.yaml`);
1013
- console.log(` 📊 Size: ${(flowsStats.size / 1024).toFixed(1)}KB`);
1014
- console.log(` 📅 Last modified: ${flowsStats.mtime.toISOString()}`);
1015
- }
1016
- } else if (verbose) {
1017
- const flowsStats = await fs.stat(flowsFile);
1018
- console.log(` ✓ Unchanged: flows.yaml`);
1019
- console.log(` 📅 Last modified: ${flowsStats.mtime.toISOString()}`);
1020
- console.log(` 📊 Size: ${(flowsStats.size / 1024).toFixed(1)}KB`);
1021
- }
1022
- } catch (error) {
1023
- if (verbose) console.log(`⚠️ Error checking flows.yaml: ${error instanceof Error ? error.message : String(error)}`);
1024
- }
1025
- }
1026
-
1027
- console.log(dirty ? `${dirty} changed file(s).` : 'Clean.');
1028
- }
1029
-
1030
- async function generateFlowsYaml(
1031
- client: AxiosInstance,
1032
- customer: CustomerConfig,
1033
- agents: Agent[],
1034
- verbose: boolean = false
1035
- ): Promise<void> {
1036
- const flowsData: FlowsYamlData = { flows: [] };
1037
-
1038
- // Calculate total flows for progress tracking
1039
- const totalFlows = agents.reduce((sum, agent) => sum + (agent.flows?.length || 0), 0);
1040
- let processedFlows = 0;
1041
-
1042
- if (!verbose && totalFlows > 0) {
1043
- console.log(`📄 Generating flows.yaml (${totalFlows} flows)...`);
1044
- }
1045
-
1046
- for (const agent of agents) {
1047
- if (verbose) console.log(` 📁 Processing agent: ${agent.idn}`);
1048
-
1049
- const agentFlows: FlowsYamlFlow[] = [];
1050
-
1051
- for (const flow of agent.flows ?? []) {
1052
- processedFlows++;
1053
-
1054
- if (verbose) {
1055
- console.log(` 📄 Processing flow: ${flow.idn}`);
1056
- } else {
1057
- // Simple progress indicator without verbose mode
1058
- const percent = Math.round((processedFlows / totalFlows) * 100);
1059
- const progressBar = '█'.repeat(Math.floor(percent / 5)) + '░'.repeat(20 - Math.floor(percent / 5));
1060
- const progressText = ` [${progressBar}] ${percent}% (${processedFlows}/${totalFlows}) ${flow.idn}`;
1061
-
1062
- // Pad the line to clear any leftover text from longer previous lines
1063
- const padding = ' '.repeat(Math.max(0, 80 - progressText.length));
1064
- process.stdout.write(`\r${progressText}${padding}`);
1065
- }
1066
-
1067
- // Get skills for this flow
1068
- const skills = await listFlowSkills(client, flow.id);
1069
- const skillsData: FlowsYamlSkill[] = skills.map(skill => ({
1070
- idn: skill.idn,
1071
- title: skill.title || "",
1072
- prompt_script: `flows/${flow.idn}/${skill.idn}.${skill.runner_type === 'nsl' ? 'jinja' : 'guidance'}`,
1073
- runner_type: `!enum "RunnerType.${skill.runner_type}"`,
1074
- model: {
1075
- model_idn: skill.model.model_idn,
1076
- provider_idn: skill.model.provider_idn
1077
- },
1078
- parameters: skill.parameters.map(param => ({
1079
- name: param.name,
1080
- default_value: param.default_value || " "
1081
- }))
1082
- }));
1083
-
1084
- // Get events for this flow
1085
- let eventsData: FlowsYamlEvent[] = [];
1086
- try {
1087
- const events = await listFlowEvents(client, flow.id);
1088
- eventsData = events.map(event => ({
1089
- title: event.description,
1090
- idn: event.idn,
1091
- skill_selector: `!enum "SkillSelector.${event.skill_selector}"`,
1092
- skill_idn: event.skill_idn || null,
1093
- state_idn: event.state_idn || null,
1094
- integration_idn: event.integration_idn || null,
1095
- connector_idn: event.connector_idn || null,
1096
- interrupt_mode: `!enum "InterruptMode.${event.interrupt_mode}"`
1097
- }));
1098
- if (verbose) console.log(` 📋 Found ${events.length} events`);
1099
- } catch (error) {
1100
- if (verbose) console.log(` ⚠️ No events found for flow ${flow.idn}`);
1101
- }
1102
-
1103
- // Get state fields for this flow
1104
- let stateFieldsData: FlowsYamlState[] = [];
1105
- try {
1106
- const states = await listFlowStates(client, flow.id);
1107
- stateFieldsData = states.map(state => ({
1108
- title: state.title,
1109
- idn: state.idn,
1110
- default_value: state.default_value || null,
1111
- scope: `!enum "StateFieldScope.${state.scope}"`
1112
- }));
1113
- if (verbose) console.log(` 📊 Found ${states.length} state fields`);
1114
- } catch (error) {
1115
- if (verbose) console.log(` ⚠️ No state fields found for flow ${flow.idn}`);
1116
- }
1117
-
1118
- agentFlows.push({
1119
- idn: flow.idn,
1120
- title: flow.title,
1121
- description: flow.description || null,
1122
- default_runner_type: `!enum "RunnerType.${flow.default_runner_type}"`,
1123
- default_provider_idn: flow.default_model.provider_idn,
1124
- default_model_idn: flow.default_model.model_idn,
1125
- skills: skillsData,
1126
- events: eventsData,
1127
- state_fields: stateFieldsData
1128
- });
1129
- }
1130
-
1131
- const agentData: FlowsYamlAgent = {
1132
- agent_idn: agent.idn,
1133
- agent_description: agent.description || null,
1134
- agent_flows: agentFlows
1135
- };
1136
-
1137
- flowsData.flows.push(agentData);
1138
- }
1139
-
1140
- // Clear progress bar and move to new line
1141
- if (!verbose && totalFlows > 0) {
1142
- process.stdout.write('\n');
1143
- }
1144
-
1145
- // Convert to YAML and write to file with custom enum handling
1146
- let yamlContent = yaml.dump(flowsData, {
1147
- indent: 2,
1148
- lineWidth: -1,
1149
- noRefs: true,
1150
- sortKeys: false,
1151
- quotingType: '"',
1152
- forceQuotes: false,
1153
- flowLevel: -1,
1154
- styles: {
1155
- '!!str': 'literal' // Use literal style for multiline strings
1156
- }
1157
- });
1158
-
1159
- // Post-process to fix enum formatting
1160
- yamlContent = yamlContent.replace(/"(!enum \\"([^"]+)\\")"/g, '!enum "$2"');
1161
-
1162
- // Post-process to fix multiline string formatting to match expected format
1163
- yamlContent = yamlContent.replace(
1164
- /^(\s+agent_description: )"([^"]*)"$/gm,
1165
- (match, indent, desc) => {
1166
- // Check for long descriptions that should be multiline
1167
- if (desc.length > 80 && desc.includes(' (clients of your business)')) {
1168
- // Split the ConvoAgent description into multiline YAML format
1169
- return `${indent}"${desc.replace(/(\. This Agent communicates with Users) \(clients of your business\)/, '$1\\\n \\ (clients of your business)')}"`;
1170
- }
1171
- if (desc.length > 100 && desc.includes('within a browser')) {
1172
- // Split the MagicWorker description into multiline YAML format
1173
- return `${indent}"${desc.replace(/(within a browser and behaving "like a human" when interacting with web applications that lack APIs\.) (This agent is often used)/, '$1\\\n \\ $2')}"`;
1174
- }
1175
- return match;
1176
- }
1177
- );
1178
-
1179
- const yamlPath = flowsYamlPath(customer.idn);
1180
- await writeFileSafe(yamlPath, yamlContent);
1181
- console.log(`✓ Generated flows.yaml`);
1182
- }
1183
-
1184
- // Conversation sync functions
1185
-
1186
- export async function pullConversations(
1187
- client: AxiosInstance,
1188
- customer: CustomerConfig,
1189
- options: ConversationOptions = {},
1190
- verbose: boolean = false
1191
- ): Promise<void> {
1192
- if (verbose) console.log(`💬 Fetching conversations for customer ${customer.idn}...`);
1193
-
1194
- try {
1195
- // Get all user personas with pagination
1196
- const allPersonas: UserPersona[] = [];
1197
- let page = 1;
1198
- const perPage = 50;
1199
- let hasMore = true;
1200
-
1201
- while (hasMore) {
1202
- const response = await listUserPersonas(client, page, perPage);
1203
- allPersonas.push(...response.items);
1204
-
1205
- if (verbose) console.log(`📋 Page ${page}: Found ${response.items.length} personas (${allPersonas.length}/${response.metadata.total} total)`);
1206
-
1207
- hasMore = response.items.length === perPage && allPersonas.length < response.metadata.total;
1208
- page++;
1209
- }
1210
-
1211
- if (options.maxPersonas && allPersonas.length > options.maxPersonas) {
1212
- allPersonas.splice(options.maxPersonas);
1213
- if (verbose) console.log(`⚠️ Limited to ${options.maxPersonas} personas as requested`);
1214
- }
1215
-
1216
- if (verbose) console.log(`👥 Processing ${allPersonas.length} personas...`);
1217
-
1218
- // Process personas concurrently with limited concurrency
1219
- const processedPersonas: ProcessedPersona[] = [];
1220
-
1221
- await Promise.all(allPersonas.map(persona => concurrencyLimit(async () => {
1222
- try {
1223
- // Extract phone number from actors
1224
- const phoneActor = persona.actors.find(actor =>
1225
- actor.integration_idn === 'newo_voice' &&
1226
- actor.connector_idn === 'newo_voice_connector' &&
1227
- actor.contact_information?.startsWith('+')
1228
- );
1229
- const phone = phoneActor?.contact_information || null;
1230
-
1231
- // Get acts for this persona
1232
- const allActs: ConversationAct[] = [];
1233
- let actPage = 1;
1234
- const actsPerPage = 100; // Higher limit for acts
1235
- let hasMoreActs = true;
1236
-
1237
- while (hasMoreActs) {
1238
- // First try the chat history API as alternative
1239
- try {
1240
- // Get user actor IDs from persona actors
1241
- const userActors = persona.actors.filter(actor =>
1242
- actor.integration_idn === 'newo_voice' &&
1243
- actor.connector_idn === 'newo_voice_connector'
1244
- );
1245
-
1246
- if (userActors.length > 0) {
1247
- const chatHistoryParams = {
1248
- user_actor_id: userActors[0]!.id,
1249
- page: actPage,
1250
- per: actsPerPage
1251
- };
1252
-
1253
- const chatResponse = await getChatHistory(client, chatHistoryParams);
1254
- if (chatResponse.items && chatResponse.items.length > 0) {
1255
- // Convert chat history format to acts format - create minimal ConversationAct objects
1256
- const convertedActs: ConversationAct[] = chatResponse.items.map((item: any) => ({
1257
- id: item.id || `chat_${Math.random()}`,
1258
- command_act_id: null,
1259
- external_event_id: item.external_event_id || 'chat_history',
1260
- arguments: [],
1261
- reference_idn: (item.is_agent === true) ? 'agent_message' : 'user_message',
1262
- runtime_context_id: item.runtime_context_id || 'chat_history',
1263
- source_text: item.payload?.text || item.message || item.content || item.text || '',
1264
- original_text: item.payload?.text || item.message || item.content || item.text || '',
1265
- datetime: item.datetime || item.created_at || item.timestamp || new Date().toISOString(),
1266
- user_actor_id: userActors[0]!.id,
1267
- agent_actor_id: null,
1268
- user_persona_id: persona.id,
1269
- user_persona_name: persona.name,
1270
- agent_persona_id: item.agent_persona_id || 'unknown',
1271
- external_id: item.external_id || null,
1272
- integration_idn: 'newo_voice',
1273
- connector_idn: 'newo_voice_connector',
1274
- to_integration_idn: null,
1275
- to_connector_idn: null,
1276
- is_agent: Boolean(item.is_agent === true),
1277
- project_idn: null,
1278
- flow_idn: item.flow_idn || 'unknown',
1279
- skill_idn: item.skill_idn || 'unknown',
1280
- session_id: item.session_id || 'unknown',
1281
- recordings: item.recordings || [],
1282
- contact_information: item.contact_information || null
1283
- }));
1284
-
1285
- allActs.push(...convertedActs);
1286
-
1287
- if (verbose && convertedActs.length > 0) {
1288
- console.log(` 👤 ${persona.name}: Chat History - ${convertedActs.length} messages (${allActs.length} total)`);
1289
- }
1290
-
1291
- hasMoreActs = chatResponse.items.length === actsPerPage &&
1292
- chatResponse.metadata?.total !== undefined &&
1293
- allActs.length < chatResponse.metadata.total;
1294
- actPage++;
1295
- continue; // Skip the original acts API call
1296
- }
1297
- }
1298
- } catch (chatError) {
1299
- if (verbose) console.log(` ⚠️ Chat history failed for ${persona.name}: ${chatError instanceof Error ? chatError.message : String(chatError)}`);
1300
- // No fallback - only use chat history API
1301
- hasMoreActs = false;
1302
- }
1303
- }
1304
-
1305
- // Sort acts by datetime ascending (chronological order)
1306
- allActs.sort((a, b) => new Date(a.datetime).getTime() - new Date(b.datetime).getTime());
1307
-
1308
- // Process acts into simplified format - exclude redundant fields
1309
- const processedActs: ProcessedAct[] = allActs.map(act => {
1310
- const processedAct: ProcessedAct = {
1311
- datetime: act.datetime,
1312
- type: act.reference_idn,
1313
- message: act.source_text
1314
- };
1315
-
1316
- // Only include non-redundant fields
1317
- if (act.contact_information) {
1318
- (processedAct as any).contact_information = act.contact_information;
1319
- }
1320
- if (act.flow_idn && act.flow_idn !== 'unknown') {
1321
- (processedAct as any).flow_idn = act.flow_idn;
1322
- }
1323
- if (act.skill_idn && act.skill_idn !== 'unknown') {
1324
- (processedAct as any).skill_idn = act.skill_idn;
1325
- }
1326
- if (act.session_id && act.session_id !== 'unknown') {
1327
- (processedAct as any).session_id = act.session_id;
1328
- }
1329
-
1330
- return processedAct;
1331
- });
1332
-
1333
- processedPersonas.push({
1334
- id: persona.id,
1335
- name: persona.name,
1336
- phone,
1337
- act_count: persona.act_count,
1338
- acts: processedActs
1339
- });
1340
-
1341
- if (verbose) console.log(` ✓ Processed ${persona.name}: ${processedActs.length} acts`);
1342
- } catch (error) {
1343
- console.error(`❌ Failed to process persona ${persona.name}:`, error);
1344
- // Continue with other personas
1345
- }
1346
- })));
1347
-
1348
- // Sort personas by most recent act time (descending) - use latest act from acts array
1349
- processedPersonas.sort((a, b) => {
1350
- const aLatestTime = a.acts.length > 0 ? a.acts[a.acts.length - 1]!.datetime : '1970-01-01T00:00:00.000Z';
1351
- const bLatestTime = b.acts.length > 0 ? b.acts[b.acts.length - 1]!.datetime : '1970-01-01T00:00:00.000Z';
1352
- return new Date(bLatestTime).getTime() - new Date(aLatestTime).getTime();
1353
- });
1354
-
1355
- // Calculate totals
1356
- const totalActs = processedPersonas.reduce((sum, persona) => sum + persona.acts.length, 0);
1357
-
1358
- // Create final conversations data
1359
- const conversationsData: ConversationsData = {
1360
- personas: processedPersonas,
1361
- total_personas: processedPersonas.length,
1362
- total_acts: totalActs,
1363
- generated_at: new Date().toISOString()
1364
- };
1365
-
1366
- // Save to YAML file
1367
- const conversationsPath = `newo_customers/${customer.idn}/conversations.yaml`;
1368
- const yamlContent = yaml.dump(conversationsData, {
1369
- indent: 2,
1370
- quotingType: '"',
1371
- forceQuotes: false,
1372
- lineWidth: 120,
1373
- noRefs: true,
1374
- sortKeys: false,
1375
- flowLevel: -1
1376
- });
1377
-
1378
- await writeFileSafe(conversationsPath, yamlContent);
1379
-
1380
- if (verbose) {
1381
- console.log(`✓ Saved conversations to ${conversationsPath}`);
1382
- console.log(`📊 Summary: ${processedPersonas.length} personas, ${totalActs} total acts`);
1383
- }
1384
-
1385
- } catch (error) {
1386
- console.error(`❌ Failed to pull conversations for ${customer.idn}:`, error);
1387
- throw error;
1388
- }
1389
- }
1
+ /**
2
+ * NEWO CLI Sync Operations - Modular architecture entry point
3
+ */
4
+
5
+ // Re-export from specialized modules
6
+ export { saveCustomerAttributes } from './sync/attributes.js';
7
+ export { pullConversations } from './sync/conversations.js';
8
+ export { status } from './sync/status.js';
9
+ export { pullSingleProject, pullAll } from './sync/projects.js';
10
+ export { pushChanged } from './sync/push.js';
11
+ export { generateFlowsYaml } from './sync/metadata.js';
12
+
13
+ // Re-export type guards for backward compatibility
14
+ export { isProjectMap, isLegacyProjectMap } from './sync/projects.js';