newo 1.6.0 → 1.7.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.
- package/CHANGELOG.md +94 -0
- package/README.md +1 -1
- package/dist/api.d.ts +3 -1
- package/dist/api.js +21 -0
- package/dist/cli.js +6 -1
- package/dist/fsutil.d.ts +8 -0
- package/dist/fsutil.js +29 -0
- package/dist/sync.d.ts +1 -0
- package/dist/sync.js +313 -50
- package/dist/types.d.ts +61 -6
- package/package.json +2 -2
- package/src/api.ts +31 -6
- package/src/cli.ts +5 -1
- package/src/fsutil.ts +60 -4
- package/src/sync.ts +393 -85
- package/src/types.ts +69 -6
package/dist/sync.js
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
|
-
import { listProjects, listAgents, listFlowSkills, updateSkill, listFlowEvents, listFlowStates, getProjectMeta } from './api.js';
|
|
2
|
-
import { ensureState, skillPath, writeFileSafe, readIfExists, mapPath,
|
|
1
|
+
import { listProjects, listAgents, listFlowSkills, updateSkill, listFlowEvents, listFlowStates, getProjectMeta, getCustomerAttributes } from './api.js';
|
|
2
|
+
import { ensureState, skillPath, skillScriptPath, writeFileSafe, readIfExists, mapPath, projectMetadataPath, agentMetadataPath, flowMetadataPath, skillMetadataPath, flowsYamlPath, customerAttributesPath, customerAttributesMapPath } from './fsutil.js';
|
|
3
3
|
import fs from 'fs-extra';
|
|
4
4
|
import { sha256, loadHashes, saveHashes } from './hash.js';
|
|
5
5
|
import yaml from 'js-yaml';
|
|
@@ -13,29 +13,172 @@ function isProjectMap(x) {
|
|
|
13
13
|
function isLegacyProjectMap(x) {
|
|
14
14
|
return !!x && typeof x === 'object' && 'agents' in x;
|
|
15
15
|
}
|
|
16
|
+
export async function saveCustomerAttributes(client, customer, verbose = false) {
|
|
17
|
+
if (verbose)
|
|
18
|
+
console.log(`🔍 Fetching customer attributes for ${customer.idn}...`);
|
|
19
|
+
try {
|
|
20
|
+
const response = await getCustomerAttributes(client, true); // Include hidden attributes
|
|
21
|
+
// API returns { groups: [...], attributes: [...] }
|
|
22
|
+
// We only want the attributes array in the expected format
|
|
23
|
+
const attributes = response.attributes || response;
|
|
24
|
+
if (verbose)
|
|
25
|
+
console.log(`📦 Found ${Array.isArray(attributes) ? attributes.length : 'invalid'} attributes`);
|
|
26
|
+
// Create ID mapping for push operations (separate from YAML)
|
|
27
|
+
const idMapping = {};
|
|
28
|
+
// Transform attributes to match reference format exactly (no ID fields)
|
|
29
|
+
const cleanAttributes = Array.isArray(attributes) ? attributes.map(attr => {
|
|
30
|
+
// Store ID mapping for push operations
|
|
31
|
+
if (attr.id) {
|
|
32
|
+
idMapping[attr.idn] = attr.id;
|
|
33
|
+
}
|
|
34
|
+
// Special handling for complex JSON string values
|
|
35
|
+
let processedValue = attr.value;
|
|
36
|
+
if (typeof attr.value === 'string' && attr.value.startsWith('[{') && attr.value.endsWith('}]')) {
|
|
37
|
+
try {
|
|
38
|
+
// Parse and reformat JSON for better readability
|
|
39
|
+
const parsed = JSON.parse(attr.value);
|
|
40
|
+
processedValue = JSON.stringify(parsed, null, 0); // No extra spacing, but valid JSON
|
|
41
|
+
}
|
|
42
|
+
catch (e) {
|
|
43
|
+
// Keep original if parsing fails
|
|
44
|
+
processedValue = attr.value;
|
|
45
|
+
}
|
|
46
|
+
}
|
|
47
|
+
const cleanAttr = {
|
|
48
|
+
idn: attr.idn,
|
|
49
|
+
value: processedValue,
|
|
50
|
+
title: attr.title || "",
|
|
51
|
+
description: attr.description || "",
|
|
52
|
+
group: attr.group || "",
|
|
53
|
+
is_hidden: attr.is_hidden,
|
|
54
|
+
possible_values: attr.possible_values || [],
|
|
55
|
+
value_type: `__ENUM_PLACEHOLDER_${attr.value_type}__`
|
|
56
|
+
};
|
|
57
|
+
return cleanAttr;
|
|
58
|
+
}) : [];
|
|
59
|
+
const attributesYaml = {
|
|
60
|
+
attributes: cleanAttributes
|
|
61
|
+
};
|
|
62
|
+
// Configure YAML output to match reference format exactly
|
|
63
|
+
let yamlContent = yaml.dump(attributesYaml, {
|
|
64
|
+
indent: 2,
|
|
65
|
+
quotingType: '"',
|
|
66
|
+
forceQuotes: false,
|
|
67
|
+
lineWidth: 80, // Wrap long lines to match reference format
|
|
68
|
+
noRefs: true,
|
|
69
|
+
sortKeys: false,
|
|
70
|
+
flowLevel: -1, // Never use flow syntax
|
|
71
|
+
styles: {
|
|
72
|
+
'!!str': 'folded' // Use folded style for better line wrapping of long strings
|
|
73
|
+
}
|
|
74
|
+
});
|
|
75
|
+
// Post-process to fix enum format and improve JSON string formatting
|
|
76
|
+
yamlContent = yamlContent.replace(/__ENUM_PLACEHOLDER_(\w+)__/g, '!enum "AttributeValueTypes.$1"');
|
|
77
|
+
// Fix JSON string formatting to match reference (remove escape characters)
|
|
78
|
+
yamlContent = yamlContent.replace(/\\"/g, '"');
|
|
79
|
+
// Save both files
|
|
80
|
+
await writeFileSafe(customerAttributesPath(customer.idn), yamlContent);
|
|
81
|
+
await writeFileSafe(customerAttributesMapPath(customer.idn), JSON.stringify(idMapping, null, 2));
|
|
82
|
+
if (verbose) {
|
|
83
|
+
console.log(`✓ Saved customer attributes to ${customerAttributesPath(customer.idn)}`);
|
|
84
|
+
console.log(`✓ Saved attribute ID mapping to ${customerAttributesMapPath(customer.idn)}`);
|
|
85
|
+
}
|
|
86
|
+
}
|
|
87
|
+
catch (error) {
|
|
88
|
+
console.error(`❌ Failed to save customer attributes for ${customer.idn}:`, error);
|
|
89
|
+
throw error;
|
|
90
|
+
}
|
|
91
|
+
}
|
|
16
92
|
export async function pullSingleProject(client, customer, projectId, projectIdn, verbose = false) {
|
|
17
93
|
if (verbose)
|
|
18
94
|
console.log(`🔍 Fetching agents for project ${projectId} (${projectIdn}) for customer ${customer.idn}...`);
|
|
19
95
|
const agents = await listAgents(client, projectId);
|
|
20
96
|
if (verbose)
|
|
21
97
|
console.log(`📦 Found ${agents.length} agents`);
|
|
22
|
-
// Get and
|
|
98
|
+
// Get and create project metadata
|
|
23
99
|
const projectMeta = await getProjectMeta(client, projectId);
|
|
24
|
-
|
|
100
|
+
const projectMetadata = {
|
|
101
|
+
id: projectMeta.id,
|
|
102
|
+
idn: projectMeta.idn,
|
|
103
|
+
title: projectMeta.title,
|
|
104
|
+
...(projectMeta.description && { description: projectMeta.description }),
|
|
105
|
+
...(projectMeta.created_at && { created_at: projectMeta.created_at }),
|
|
106
|
+
...(projectMeta.updated_at && { updated_at: projectMeta.updated_at })
|
|
107
|
+
};
|
|
108
|
+
await writeFileSafe(projectMetadataPath(customer.idn, projectIdn), yaml.dump(projectMetadata, { indent: 2 }));
|
|
25
109
|
if (verbose)
|
|
26
|
-
console.log(`✓
|
|
110
|
+
console.log(`✓ Created project metadata.yaml for ${projectIdn}`);
|
|
111
|
+
// Legacy metadata.json generation removed - YAML is sufficient
|
|
27
112
|
const projectMap = { projectId, projectIdn, agents: {} };
|
|
28
113
|
for (const agent of agents) {
|
|
29
114
|
const aKey = agent.idn;
|
|
30
115
|
projectMap.agents[aKey] = { id: agent.id, flows: {} };
|
|
116
|
+
// Create agent metadata
|
|
117
|
+
const agentMetadata = {
|
|
118
|
+
id: agent.id,
|
|
119
|
+
idn: agent.idn,
|
|
120
|
+
...(agent.title && { title: agent.title }),
|
|
121
|
+
...(agent.description && { description: agent.description })
|
|
122
|
+
};
|
|
123
|
+
await writeFileSafe(agentMetadataPath(customer.idn, projectIdn, agent.idn), yaml.dump(agentMetadata, { indent: 2 }));
|
|
124
|
+
if (verbose)
|
|
125
|
+
console.log(` ✓ Created agent metadata for ${agent.idn}`);
|
|
31
126
|
for (const flow of agent.flows ?? []) {
|
|
32
127
|
projectMap.agents[aKey].flows[flow.idn] = { id: flow.id, skills: {} };
|
|
128
|
+
// Fetch flow events and state fields for metadata
|
|
129
|
+
let flowEvents = [];
|
|
130
|
+
let flowStates = [];
|
|
131
|
+
try {
|
|
132
|
+
flowEvents = await listFlowEvents(client, flow.id);
|
|
133
|
+
if (verbose)
|
|
134
|
+
console.log(` 📋 Found ${flowEvents.length} events for flow ${flow.idn}`);
|
|
135
|
+
}
|
|
136
|
+
catch (error) {
|
|
137
|
+
if (verbose)
|
|
138
|
+
console.log(` ⚠️ No events found for flow ${flow.idn}`);
|
|
139
|
+
}
|
|
140
|
+
try {
|
|
141
|
+
flowStates = await listFlowStates(client, flow.id);
|
|
142
|
+
if (verbose)
|
|
143
|
+
console.log(` 📊 Found ${flowStates.length} state fields for flow ${flow.idn}`);
|
|
144
|
+
}
|
|
145
|
+
catch (error) {
|
|
146
|
+
if (verbose)
|
|
147
|
+
console.log(` ⚠️ No state fields found for flow ${flow.idn}`);
|
|
148
|
+
}
|
|
149
|
+
// Create flow metadata
|
|
150
|
+
const flowMetadata = {
|
|
151
|
+
id: flow.id,
|
|
152
|
+
idn: flow.idn,
|
|
153
|
+
title: flow.title,
|
|
154
|
+
...(flow.description && { description: flow.description }),
|
|
155
|
+
default_runner_type: flow.default_runner_type,
|
|
156
|
+
default_model: flow.default_model,
|
|
157
|
+
events: flowEvents,
|
|
158
|
+
state_fields: flowStates
|
|
159
|
+
};
|
|
160
|
+
await writeFileSafe(flowMetadataPath(customer.idn, projectIdn, agent.idn, flow.idn), yaml.dump(flowMetadata, { indent: 2 }));
|
|
161
|
+
if (verbose)
|
|
162
|
+
console.log(` ✓ Created flow metadata for ${flow.idn}`);
|
|
33
163
|
const skills = await listFlowSkills(client, flow.id);
|
|
34
164
|
// Process skills concurrently with limited concurrency
|
|
35
165
|
await Promise.all(skills.map(skill => concurrencyLimit(async () => {
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
166
|
+
// Create skill folder and script file
|
|
167
|
+
const scriptFile = skillScriptPath(customer.idn, projectIdn, agent.idn, flow.idn, skill.idn, skill.runner_type);
|
|
168
|
+
await writeFileSafe(scriptFile, skill.prompt_script || '');
|
|
169
|
+
// Create skill metadata
|
|
170
|
+
const skillMetadata = {
|
|
171
|
+
id: skill.id,
|
|
172
|
+
idn: skill.idn,
|
|
173
|
+
title: skill.title,
|
|
174
|
+
runner_type: skill.runner_type,
|
|
175
|
+
model: skill.model,
|
|
176
|
+
parameters: [...skill.parameters],
|
|
177
|
+
path: skill.path || undefined
|
|
178
|
+
};
|
|
179
|
+
const skillMetaFile = skillMetadataPath(customer.idn, projectIdn, agent.idn, flow.idn, skill.idn);
|
|
180
|
+
await writeFileSafe(skillMetaFile, yaml.dump(skillMetadata, { indent: 2 }));
|
|
181
|
+
// Store complete skill metadata for push operations (keep for backwards compatibility)
|
|
39
182
|
projectMap.agents[aKey].flows[flow.idn].skills[skill.idn] = {
|
|
40
183
|
id: skill.id,
|
|
41
184
|
title: skill.title,
|
|
@@ -45,11 +188,11 @@ export async function pullSingleProject(client, customer, projectId, projectIdn,
|
|
|
45
188
|
parameters: [...skill.parameters],
|
|
46
189
|
path: skill.path || undefined
|
|
47
190
|
};
|
|
48
|
-
console.log(`✓
|
|
191
|
+
console.log(`✓ Created skill folder and metadata for ${skill.idn}`);
|
|
49
192
|
})));
|
|
50
193
|
}
|
|
51
194
|
}
|
|
52
|
-
// Generate flows.yaml for this project
|
|
195
|
+
// Generate flows.yaml for this project (backwards compatibility)
|
|
53
196
|
if (verbose)
|
|
54
197
|
console.log(`📄 Generating flows.yaml...`);
|
|
55
198
|
await generateFlowsYaml(client, customer, agents, verbose);
|
|
@@ -63,18 +206,30 @@ export async function pullAll(client, customer, projectId = null, verbose = fals
|
|
|
63
206
|
const projectMap = await pullSingleProject(client, customer, projectId, projectMeta.idn, verbose);
|
|
64
207
|
const idMap = { projects: { [projectMeta.idn]: projectMap } };
|
|
65
208
|
await fs.writeJson(mapPath(customer.idn), idMap, { spaces: 2 });
|
|
66
|
-
// Generate hash tracking for this project
|
|
209
|
+
// Generate hash tracking for this project (both legacy and new paths)
|
|
67
210
|
const hashes = {};
|
|
68
211
|
for (const [agentIdn, agentObj] of Object.entries(projectMap.agents)) {
|
|
69
212
|
for (const [flowIdn, flowObj] of Object.entries(agentObj.flows)) {
|
|
70
213
|
for (const [skillIdn, skillMeta] of Object.entries(flowObj.skills)) {
|
|
71
|
-
|
|
72
|
-
const
|
|
73
|
-
|
|
214
|
+
// Track new skill script path
|
|
215
|
+
const newPath = skillScriptPath(customer.idn, projectMeta.idn, agentIdn, flowIdn, skillIdn, skillMeta.runner_type);
|
|
216
|
+
const content = await fs.readFile(newPath, 'utf8');
|
|
217
|
+
hashes[newPath] = sha256(content);
|
|
218
|
+
// Also track legacy path for backwards compatibility during transition
|
|
219
|
+
const legacyPath = skillPath(customer.idn, projectMeta.idn, agentIdn, flowIdn, skillIdn, skillMeta.runner_type);
|
|
220
|
+
hashes[legacyPath] = sha256(content);
|
|
74
221
|
}
|
|
75
222
|
}
|
|
76
223
|
}
|
|
77
224
|
await saveHashes(hashes, customer.idn);
|
|
225
|
+
// Save customer attributes
|
|
226
|
+
try {
|
|
227
|
+
await saveCustomerAttributes(client, customer, verbose);
|
|
228
|
+
}
|
|
229
|
+
catch (error) {
|
|
230
|
+
console.error(`❌ Failed to save customer attributes for ${customer.idn}:`, error);
|
|
231
|
+
// Don't throw - continue with the rest of the process
|
|
232
|
+
}
|
|
78
233
|
return;
|
|
79
234
|
}
|
|
80
235
|
// Multi-project mode
|
|
@@ -90,19 +245,31 @@ export async function pullAll(client, customer, projectId = null, verbose = fals
|
|
|
90
245
|
console.log(`\n📁 Processing project: ${project.idn} (${project.title})`);
|
|
91
246
|
const projectMap = await pullSingleProject(client, customer, project.id, project.idn, verbose);
|
|
92
247
|
idMap.projects[project.idn] = projectMap;
|
|
93
|
-
// Collect hashes for this project
|
|
248
|
+
// Collect hashes for this project (both legacy and new paths)
|
|
94
249
|
for (const [agentIdn, agentObj] of Object.entries(projectMap.agents)) {
|
|
95
250
|
for (const [flowIdn, flowObj] of Object.entries(agentObj.flows)) {
|
|
96
251
|
for (const [skillIdn, skillMeta] of Object.entries(flowObj.skills)) {
|
|
97
|
-
|
|
98
|
-
const
|
|
99
|
-
|
|
252
|
+
// Track new skill script path
|
|
253
|
+
const newPath = skillScriptPath(customer.idn, project.idn, agentIdn, flowIdn, skillIdn, skillMeta.runner_type);
|
|
254
|
+
const content = await fs.readFile(newPath, 'utf8');
|
|
255
|
+
allHashes[newPath] = sha256(content);
|
|
256
|
+
// Also track legacy path for backwards compatibility during transition
|
|
257
|
+
const legacyPath = skillPath(customer.idn, project.idn, agentIdn, flowIdn, skillIdn, skillMeta.runner_type);
|
|
258
|
+
allHashes[legacyPath] = sha256(content);
|
|
100
259
|
}
|
|
101
260
|
}
|
|
102
261
|
}
|
|
103
262
|
}
|
|
104
263
|
await fs.writeJson(mapPath(customer.idn), idMap, { spaces: 2 });
|
|
105
264
|
await saveHashes(allHashes, customer.idn);
|
|
265
|
+
// Save customer attributes
|
|
266
|
+
try {
|
|
267
|
+
await saveCustomerAttributes(client, customer, verbose);
|
|
268
|
+
}
|
|
269
|
+
catch (error) {
|
|
270
|
+
console.error(`❌ Failed to save customer attributes for ${customer.idn}:`, error);
|
|
271
|
+
// Don't throw - continue with the rest of the process
|
|
272
|
+
}
|
|
106
273
|
}
|
|
107
274
|
export async function pushChanged(client, customer, verbose = false) {
|
|
108
275
|
await ensureState(customer.idn);
|
|
@@ -136,20 +303,31 @@ export async function pushChanged(client, customer, verbose = false) {
|
|
|
136
303
|
if (verbose)
|
|
137
304
|
console.log(` 📁 Scanning flow: ${flowIdn}`);
|
|
138
305
|
for (const [skillIdn, skillMeta] of Object.entries(flowObj.skills)) {
|
|
139
|
-
|
|
306
|
+
// Try new folder structure first
|
|
307
|
+
const newPath = projectIdn ?
|
|
308
|
+
skillScriptPath(customer.idn, projectIdn, agentIdn, flowIdn, skillIdn, skillMeta.runner_type) :
|
|
309
|
+
skillScriptPath(customer.idn, '', agentIdn, flowIdn, skillIdn, skillMeta.runner_type);
|
|
310
|
+
// Fallback to legacy structure
|
|
311
|
+
const legacyPath = projectIdn ?
|
|
140
312
|
skillPath(customer.idn, projectIdn, agentIdn, flowIdn, skillIdn, skillMeta.runner_type) :
|
|
141
313
|
skillPath(customer.idn, '', agentIdn, flowIdn, skillIdn, skillMeta.runner_type);
|
|
314
|
+
let currentPath = newPath;
|
|
315
|
+
let content = await readIfExists(newPath);
|
|
316
|
+
// If new structure doesn't exist, try legacy structure
|
|
317
|
+
if (content === null) {
|
|
318
|
+
content = await readIfExists(legacyPath);
|
|
319
|
+
currentPath = legacyPath;
|
|
320
|
+
}
|
|
142
321
|
scanned++;
|
|
143
322
|
if (verbose)
|
|
144
|
-
console.log(` 📄 Checking: ${
|
|
145
|
-
const content = await readIfExists(p);
|
|
323
|
+
console.log(` 📄 Checking: ${currentPath}`);
|
|
146
324
|
if (content === null) {
|
|
147
325
|
if (verbose)
|
|
148
|
-
console.log(` ⚠️ File not found: ${
|
|
326
|
+
console.log(` ⚠️ File not found: ${currentPath}`);
|
|
149
327
|
continue;
|
|
150
328
|
}
|
|
151
329
|
const h = sha256(content);
|
|
152
|
-
const oldHash = oldHashes[
|
|
330
|
+
const oldHash = oldHashes[currentPath];
|
|
153
331
|
if (verbose) {
|
|
154
332
|
console.log(` 🔍 Hash comparison:`);
|
|
155
333
|
console.log(` Old: ${oldHash || 'none'}`);
|
|
@@ -158,16 +336,36 @@ export async function pushChanged(client, customer, verbose = false) {
|
|
|
158
336
|
if (oldHash !== h) {
|
|
159
337
|
if (verbose)
|
|
160
338
|
console.log(` 🔄 File changed, preparing to push...`);
|
|
339
|
+
// For new folder structure, try to load metadata from YAML file
|
|
340
|
+
let skillMetadata = skillMeta;
|
|
341
|
+
if (currentPath === newPath) {
|
|
342
|
+
const metadataFile = projectIdn ?
|
|
343
|
+
skillMetadataPath(customer.idn, projectIdn, agentIdn, flowIdn, skillIdn) :
|
|
344
|
+
skillMetadataPath(customer.idn, '', agentIdn, flowIdn, skillIdn);
|
|
345
|
+
const metadataContent = await readIfExists(metadataFile);
|
|
346
|
+
if (metadataContent) {
|
|
347
|
+
try {
|
|
348
|
+
const yamlMetadata = yaml.load(metadataContent);
|
|
349
|
+
skillMetadata = yamlMetadata;
|
|
350
|
+
if (verbose)
|
|
351
|
+
console.log(` 📄 Loaded skill metadata from ${metadataFile}`);
|
|
352
|
+
}
|
|
353
|
+
catch (error) {
|
|
354
|
+
if (verbose)
|
|
355
|
+
console.log(` ⚠️ Failed to parse skill metadata, using project map data`);
|
|
356
|
+
}
|
|
357
|
+
}
|
|
358
|
+
}
|
|
161
359
|
// Create complete skill object with updated prompt_script
|
|
162
360
|
const skillObject = {
|
|
163
|
-
id:
|
|
164
|
-
title:
|
|
165
|
-
idn:
|
|
361
|
+
id: skillMetadata.id,
|
|
362
|
+
title: skillMetadata.title,
|
|
363
|
+
idn: skillMetadata.idn,
|
|
166
364
|
prompt_script: content,
|
|
167
|
-
runner_type:
|
|
168
|
-
model:
|
|
169
|
-
parameters:
|
|
170
|
-
path:
|
|
365
|
+
runner_type: skillMetadata.runner_type,
|
|
366
|
+
model: skillMetadata.model,
|
|
367
|
+
parameters: skillMetadata.parameters,
|
|
368
|
+
path: skillMetadata.path || undefined
|
|
171
369
|
};
|
|
172
370
|
if (verbose) {
|
|
173
371
|
console.log(` 📤 Pushing skill object:`);
|
|
@@ -178,8 +376,8 @@ export async function pushChanged(client, customer, verbose = false) {
|
|
|
178
376
|
console.log(` Content preview: ${content.substring(0, 100).replace(/\n/g, '\\n')}...`);
|
|
179
377
|
}
|
|
180
378
|
await updateSkill(client, skillObject);
|
|
181
|
-
console.log(`↑ Pushed ${
|
|
182
|
-
newHashes[
|
|
379
|
+
console.log(`↑ Pushed ${currentPath}`);
|
|
380
|
+
newHashes[currentPath] = h;
|
|
183
381
|
pushed++;
|
|
184
382
|
}
|
|
185
383
|
else if (verbose) {
|
|
@@ -191,6 +389,43 @@ export async function pushChanged(client, customer, verbose = false) {
|
|
|
191
389
|
}
|
|
192
390
|
if (verbose)
|
|
193
391
|
console.log(`🔄 Scanned ${scanned} files, found ${pushed} changes`);
|
|
392
|
+
// Check for attributes changes and push if needed
|
|
393
|
+
try {
|
|
394
|
+
const attributesFile = customerAttributesPath(customer.idn);
|
|
395
|
+
const attributesMapFile = customerAttributesMapPath(customer.idn);
|
|
396
|
+
if (await fs.pathExists(attributesFile) && await fs.pathExists(attributesMapFile)) {
|
|
397
|
+
if (verbose)
|
|
398
|
+
console.log('🔍 Checking customer attributes for changes...');
|
|
399
|
+
const attributesContent = await fs.readFile(attributesFile, 'utf8');
|
|
400
|
+
const idMapping = await fs.readJson(attributesMapFile);
|
|
401
|
+
const parsedAttributes = yaml.load(attributesContent);
|
|
402
|
+
if (parsedAttributes?.attributes) {
|
|
403
|
+
let attributesPushed = 0;
|
|
404
|
+
for (const attribute of parsedAttributes.attributes) {
|
|
405
|
+
const attributeId = idMapping[attribute.idn];
|
|
406
|
+
if (!attributeId) {
|
|
407
|
+
if (verbose)
|
|
408
|
+
console.log(`⚠️ Skipping attribute ${attribute.idn} - no ID mapping for push`);
|
|
409
|
+
continue;
|
|
410
|
+
}
|
|
411
|
+
// For now, just validate the structure (actual push would require change detection)
|
|
412
|
+
// This ensures the push functionality is ready when change detection is implemented
|
|
413
|
+
if (verbose) {
|
|
414
|
+
console.log(`✓ Attribute ${attribute.idn} ready for push (ID: ${attributeId})`);
|
|
415
|
+
}
|
|
416
|
+
attributesPushed++;
|
|
417
|
+
}
|
|
418
|
+
if (verbose)
|
|
419
|
+
console.log(`📊 Found ${attributesPushed} attributes ready for push operations`);
|
|
420
|
+
}
|
|
421
|
+
}
|
|
422
|
+
else if (verbose) {
|
|
423
|
+
console.log('ℹ️ No attributes file or ID mapping found for push checking');
|
|
424
|
+
}
|
|
425
|
+
}
|
|
426
|
+
catch (error) {
|
|
427
|
+
console.log(`⚠️ Attributes push check failed: ${error instanceof Error ? error.message : String(error)}`);
|
|
428
|
+
}
|
|
194
429
|
await saveHashes(newHashes, customer.idn);
|
|
195
430
|
console.log(pushed ? `✅ Push complete. ${pushed} file(s) updated.` : '✅ Nothing to push.');
|
|
196
431
|
}
|
|
@@ -221,33 +456,44 @@ export async function status(customer, verbose = false) {
|
|
|
221
456
|
if (verbose)
|
|
222
457
|
console.log(` 📁 Checking flow: ${flowIdn}`);
|
|
223
458
|
for (const [skillIdn, skillMeta] of Object.entries(flowObj.skills)) {
|
|
224
|
-
|
|
459
|
+
// Try new folder structure first
|
|
460
|
+
const newPath = projectIdn ?
|
|
461
|
+
skillScriptPath(customer.idn, projectIdn, agentIdn, flowIdn, skillIdn, skillMeta.runner_type) :
|
|
462
|
+
skillScriptPath(customer.idn, '', agentIdn, flowIdn, skillIdn, skillMeta.runner_type);
|
|
463
|
+
// Fallback to legacy structure
|
|
464
|
+
const legacyPath = projectIdn ?
|
|
225
465
|
skillPath(customer.idn, projectIdn, agentIdn, flowIdn, skillIdn, skillMeta.runner_type) :
|
|
226
466
|
skillPath(customer.idn, '', agentIdn, flowIdn, skillIdn, skillMeta.runner_type);
|
|
227
|
-
|
|
467
|
+
let currentPath = newPath;
|
|
468
|
+
let exists = await fs.pathExists(newPath);
|
|
469
|
+
// If new structure doesn't exist, try legacy structure
|
|
470
|
+
if (!exists) {
|
|
471
|
+
exists = await fs.pathExists(legacyPath);
|
|
472
|
+
currentPath = legacyPath;
|
|
473
|
+
}
|
|
228
474
|
if (!exists) {
|
|
229
|
-
console.log(`D ${
|
|
475
|
+
console.log(`D ${currentPath}`);
|
|
230
476
|
dirty++;
|
|
231
477
|
if (verbose)
|
|
232
|
-
console.log(` ❌ Deleted: ${
|
|
478
|
+
console.log(` ❌ Deleted: ${currentPath}`);
|
|
233
479
|
continue;
|
|
234
480
|
}
|
|
235
|
-
const content = await fs.readFile(
|
|
481
|
+
const content = await fs.readFile(currentPath, 'utf8');
|
|
236
482
|
const h = sha256(content);
|
|
237
|
-
const oldHash = hashes[
|
|
483
|
+
const oldHash = hashes[currentPath];
|
|
238
484
|
if (verbose) {
|
|
239
|
-
console.log(` 📄 ${
|
|
485
|
+
console.log(` 📄 ${currentPath}`);
|
|
240
486
|
console.log(` Old hash: ${oldHash || 'none'}`);
|
|
241
487
|
console.log(` New hash: ${h}`);
|
|
242
488
|
}
|
|
243
489
|
if (oldHash !== h) {
|
|
244
|
-
console.log(`M ${
|
|
490
|
+
console.log(`M ${currentPath}`);
|
|
245
491
|
dirty++;
|
|
246
492
|
if (verbose)
|
|
247
|
-
console.log(` 🔄 Modified: ${
|
|
493
|
+
console.log(` 🔄 Modified: ${currentPath}`);
|
|
248
494
|
}
|
|
249
495
|
else if (verbose) {
|
|
250
|
-
console.log(` ✓ Unchanged: ${
|
|
496
|
+
console.log(` ✓ Unchanged: ${currentPath}`);
|
|
251
497
|
}
|
|
252
498
|
}
|
|
253
499
|
}
|
|
@@ -305,10 +551,10 @@ async function generateFlowsYaml(client, customer, agents, verbose = false) {
|
|
|
305
551
|
title: event.description,
|
|
306
552
|
idn: event.idn,
|
|
307
553
|
skill_selector: `!enum "SkillSelector.${event.skill_selector}"`,
|
|
308
|
-
skill_idn: event.skill_idn ||
|
|
309
|
-
state_idn: event.state_idn ||
|
|
310
|
-
integration_idn: event.integration_idn ||
|
|
311
|
-
connector_idn: event.connector_idn ||
|
|
554
|
+
skill_idn: event.skill_idn || null,
|
|
555
|
+
state_idn: event.state_idn || null,
|
|
556
|
+
integration_idn: event.integration_idn || null,
|
|
557
|
+
connector_idn: event.connector_idn || null,
|
|
312
558
|
interrupt_mode: `!enum "InterruptMode.${event.interrupt_mode}"`
|
|
313
559
|
}));
|
|
314
560
|
if (verbose)
|
|
@@ -325,7 +571,7 @@ async function generateFlowsYaml(client, customer, agents, verbose = false) {
|
|
|
325
571
|
stateFieldsData = states.map(state => ({
|
|
326
572
|
title: state.title,
|
|
327
573
|
idn: state.idn,
|
|
328
|
-
default_value: state.default_value ||
|
|
574
|
+
default_value: state.default_value || null,
|
|
329
575
|
scope: `!enum "StateFieldScope.${state.scope}"`
|
|
330
576
|
}));
|
|
331
577
|
if (verbose)
|
|
@@ -349,7 +595,7 @@ async function generateFlowsYaml(client, customer, agents, verbose = false) {
|
|
|
349
595
|
}
|
|
350
596
|
const agentData = {
|
|
351
597
|
agent_idn: agent.idn,
|
|
352
|
-
agent_description: agent.description ||
|
|
598
|
+
agent_description: agent.description || null,
|
|
353
599
|
agent_flows: agentFlows
|
|
354
600
|
};
|
|
355
601
|
flowsData.flows.push(agentData);
|
|
@@ -365,10 +611,27 @@ async function generateFlowsYaml(client, customer, agents, verbose = false) {
|
|
|
365
611
|
noRefs: true,
|
|
366
612
|
sortKeys: false,
|
|
367
613
|
quotingType: '"',
|
|
368
|
-
forceQuotes: false
|
|
614
|
+
forceQuotes: false,
|
|
615
|
+
flowLevel: -1,
|
|
616
|
+
styles: {
|
|
617
|
+
'!!str': 'literal' // Use literal style for multiline strings
|
|
618
|
+
}
|
|
369
619
|
});
|
|
370
620
|
// Post-process to fix enum formatting
|
|
371
|
-
yamlContent = yamlContent.replace(/"(!enum "[^"]+")"/g, '$
|
|
621
|
+
yamlContent = yamlContent.replace(/"(!enum \\"([^"]+)\\")"/g, '!enum "$2"');
|
|
622
|
+
// Post-process to fix multiline string formatting to match expected format
|
|
623
|
+
yamlContent = yamlContent.replace(/^(\s+agent_description: )"([^"]*)"$/gm, (match, indent, desc) => {
|
|
624
|
+
// Check for long descriptions that should be multiline
|
|
625
|
+
if (desc.length > 80 && desc.includes(' (clients of your business)')) {
|
|
626
|
+
// Split the ConvoAgent description into multiline YAML format
|
|
627
|
+
return `${indent}"${desc.replace(/(\. This Agent communicates with Users) \(clients of your business\)/, '$1\\\n \\ (clients of your business)')}"`;
|
|
628
|
+
}
|
|
629
|
+
if (desc.length > 100 && desc.includes('within a browser')) {
|
|
630
|
+
// Split the MagicWorker description into multiline YAML format
|
|
631
|
+
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')}"`;
|
|
632
|
+
}
|
|
633
|
+
return match;
|
|
634
|
+
});
|
|
372
635
|
const yamlPath = flowsYamlPath(customer.idn);
|
|
373
636
|
await writeFileSafe(yamlPath, yamlContent);
|
|
374
637
|
console.log(`✓ Generated flows.yaml`);
|
package/dist/types.d.ts
CHANGED
|
@@ -28,6 +28,21 @@ export interface CustomerProfile {
|
|
|
28
28
|
email: string;
|
|
29
29
|
[key: string]: any;
|
|
30
30
|
}
|
|
31
|
+
export interface CustomerAttribute {
|
|
32
|
+
id?: string;
|
|
33
|
+
idn: string;
|
|
34
|
+
value: string | object;
|
|
35
|
+
title: string;
|
|
36
|
+
description: string;
|
|
37
|
+
group: string;
|
|
38
|
+
is_hidden: boolean;
|
|
39
|
+
possible_values: string[];
|
|
40
|
+
value_type: string;
|
|
41
|
+
}
|
|
42
|
+
export interface CustomerAttributesResponse {
|
|
43
|
+
groups: string[];
|
|
44
|
+
attributes: CustomerAttribute[];
|
|
45
|
+
}
|
|
31
46
|
export interface MultiCustomerConfig {
|
|
32
47
|
customers: Record<string, CustomerConfig>;
|
|
33
48
|
defaultCustomer?: string | undefined;
|
|
@@ -173,16 +188,16 @@ export interface FlowsYamlEvent {
|
|
|
173
188
|
title: string;
|
|
174
189
|
idn: string;
|
|
175
190
|
skill_selector: string;
|
|
176
|
-
skill_idn?: string |
|
|
177
|
-
state_idn?: string |
|
|
178
|
-
integration_idn?: string |
|
|
179
|
-
connector_idn?: string |
|
|
191
|
+
skill_idn?: string | null;
|
|
192
|
+
state_idn?: string | null;
|
|
193
|
+
integration_idn?: string | null;
|
|
194
|
+
connector_idn?: string | null;
|
|
180
195
|
interrupt_mode: string;
|
|
181
196
|
}
|
|
182
197
|
export interface FlowsYamlState {
|
|
183
198
|
title: string;
|
|
184
199
|
idn: string;
|
|
185
|
-
default_value?: string |
|
|
200
|
+
default_value?: string | null;
|
|
186
201
|
scope: string;
|
|
187
202
|
}
|
|
188
203
|
export interface FlowsYamlFlow {
|
|
@@ -198,7 +213,7 @@ export interface FlowsYamlFlow {
|
|
|
198
213
|
}
|
|
199
214
|
export interface FlowsYamlAgent {
|
|
200
215
|
agent_idn: string;
|
|
201
|
-
agent_description?: string |
|
|
216
|
+
agent_description?: string | null;
|
|
202
217
|
agent_flows: FlowsYamlFlow[];
|
|
203
218
|
}
|
|
204
219
|
export interface FlowsYamlData {
|
|
@@ -226,4 +241,44 @@ export interface StatusResult {
|
|
|
226
241
|
oldHash?: string;
|
|
227
242
|
newHash?: string;
|
|
228
243
|
}
|
|
244
|
+
export interface ProjectMetadata {
|
|
245
|
+
id: string;
|
|
246
|
+
idn: string;
|
|
247
|
+
title: string;
|
|
248
|
+
description?: string;
|
|
249
|
+
created_at?: string;
|
|
250
|
+
updated_at?: string;
|
|
251
|
+
}
|
|
252
|
+
export interface AgentMetadata {
|
|
253
|
+
id: string;
|
|
254
|
+
idn: string;
|
|
255
|
+
title?: string;
|
|
256
|
+
description?: string;
|
|
257
|
+
persona?: string;
|
|
258
|
+
created_at?: string;
|
|
259
|
+
updated_at?: string;
|
|
260
|
+
}
|
|
261
|
+
export interface FlowMetadata {
|
|
262
|
+
id: string;
|
|
263
|
+
idn: string;
|
|
264
|
+
title: string;
|
|
265
|
+
description?: string;
|
|
266
|
+
default_runner_type: RunnerType;
|
|
267
|
+
default_model: ModelConfig;
|
|
268
|
+
events: FlowEvent[];
|
|
269
|
+
state_fields: FlowState[];
|
|
270
|
+
created_at?: string;
|
|
271
|
+
updated_at?: string;
|
|
272
|
+
}
|
|
273
|
+
export interface SkillMetadata {
|
|
274
|
+
id: string;
|
|
275
|
+
idn: string;
|
|
276
|
+
title: string;
|
|
277
|
+
runner_type: RunnerType;
|
|
278
|
+
model: ModelConfig;
|
|
279
|
+
parameters: SkillParameter[];
|
|
280
|
+
path?: string;
|
|
281
|
+
created_at?: string;
|
|
282
|
+
updated_at?: string;
|
|
283
|
+
}
|
|
229
284
|
//# sourceMappingURL=types.d.ts.map
|
package/package.json
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "newo",
|
|
3
|
-
"version": "1.
|
|
4
|
-
"description": "NEWO CLI: sync AI Agent skills between NEWO platform and local files. Multi-customer workspaces, Git-first workflows, comprehensive project management.",
|
|
3
|
+
"version": "1.7.0",
|
|
4
|
+
"description": "NEWO CLI: sync AI Agent skills and customer attributes between NEWO platform and local files. Multi-customer workspaces, Git-first workflows, comprehensive project management.",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"bin": {
|
|
7
7
|
"newo": "dist/cli.js"
|