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/src/sync.ts
CHANGED
|
@@ -1,30 +1,37 @@
|
|
|
1
|
-
import {
|
|
2
|
-
listProjects,
|
|
3
|
-
listAgents,
|
|
4
|
-
listFlowSkills,
|
|
5
|
-
updateSkill,
|
|
6
|
-
listFlowEvents,
|
|
7
|
-
listFlowStates,
|
|
8
|
-
getProjectMeta
|
|
1
|
+
import {
|
|
2
|
+
listProjects,
|
|
3
|
+
listAgents,
|
|
4
|
+
listFlowSkills,
|
|
5
|
+
updateSkill,
|
|
6
|
+
listFlowEvents,
|
|
7
|
+
listFlowStates,
|
|
8
|
+
getProjectMeta,
|
|
9
|
+
getCustomerAttributes
|
|
9
10
|
} from './api.js';
|
|
10
|
-
import {
|
|
11
|
-
ensureState,
|
|
12
|
-
skillPath,
|
|
13
|
-
|
|
14
|
-
|
|
11
|
+
import {
|
|
12
|
+
ensureState,
|
|
13
|
+
skillPath,
|
|
14
|
+
skillScriptPath,
|
|
15
|
+
writeFileSafe,
|
|
16
|
+
readIfExists,
|
|
15
17
|
mapPath,
|
|
16
|
-
|
|
17
|
-
|
|
18
|
+
projectMetadataPath,
|
|
19
|
+
agentMetadataPath,
|
|
20
|
+
flowMetadataPath,
|
|
21
|
+
skillMetadataPath,
|
|
22
|
+
flowsYamlPath,
|
|
23
|
+
customerAttributesPath,
|
|
24
|
+
customerAttributesMapPath
|
|
18
25
|
} from './fsutil.js';
|
|
19
26
|
import fs from 'fs-extra';
|
|
20
27
|
import { sha256, loadHashes, saveHashes } from './hash.js';
|
|
21
28
|
import yaml from 'js-yaml';
|
|
22
29
|
import pLimit from 'p-limit';
|
|
23
30
|
import type { AxiosInstance } from 'axios';
|
|
24
|
-
import type {
|
|
25
|
-
Agent,
|
|
26
|
-
ProjectData,
|
|
27
|
-
ProjectMap,
|
|
31
|
+
import type {
|
|
32
|
+
Agent,
|
|
33
|
+
ProjectData,
|
|
34
|
+
ProjectMap,
|
|
28
35
|
LegacyProjectMap,
|
|
29
36
|
HashStore,
|
|
30
37
|
FlowsYamlData,
|
|
@@ -33,7 +40,14 @@ import type {
|
|
|
33
40
|
FlowsYamlSkill,
|
|
34
41
|
FlowsYamlEvent,
|
|
35
42
|
FlowsYamlState,
|
|
36
|
-
CustomerConfig
|
|
43
|
+
CustomerConfig,
|
|
44
|
+
ProjectMetadata,
|
|
45
|
+
AgentMetadata,
|
|
46
|
+
FlowMetadata,
|
|
47
|
+
SkillMetadata,
|
|
48
|
+
FlowEvent,
|
|
49
|
+
FlowState,
|
|
50
|
+
CustomerAttribute
|
|
37
51
|
} from './types.js';
|
|
38
52
|
|
|
39
53
|
// Concurrency limits for API operations
|
|
@@ -48,21 +62,120 @@ function isLegacyProjectMap(x: unknown): x is LegacyProjectMap {
|
|
|
48
62
|
return !!x && typeof x === 'object' && 'agents' in x;
|
|
49
63
|
}
|
|
50
64
|
|
|
65
|
+
export async function saveCustomerAttributes(
|
|
66
|
+
client: AxiosInstance,
|
|
67
|
+
customer: CustomerConfig,
|
|
68
|
+
verbose: boolean = false
|
|
69
|
+
): Promise<void> {
|
|
70
|
+
if (verbose) console.log(`đ Fetching customer attributes for ${customer.idn}...`);
|
|
71
|
+
|
|
72
|
+
try {
|
|
73
|
+
const response = await getCustomerAttributes(client, true); // Include hidden attributes
|
|
74
|
+
|
|
75
|
+
// API returns { groups: [...], attributes: [...] }
|
|
76
|
+
// We only want the attributes array in the expected format
|
|
77
|
+
const attributes = response.attributes || response;
|
|
78
|
+
if (verbose) console.log(`đĻ Found ${Array.isArray(attributes) ? attributes.length : 'invalid'} attributes`);
|
|
79
|
+
|
|
80
|
+
// Create ID mapping for push operations (separate from YAML)
|
|
81
|
+
const idMapping: Record<string, string> = {};
|
|
82
|
+
|
|
83
|
+
// Transform attributes to match reference format exactly (no ID fields)
|
|
84
|
+
const cleanAttributes = Array.isArray(attributes) ? attributes.map(attr => {
|
|
85
|
+
// Store ID mapping for push operations
|
|
86
|
+
if (attr.id) {
|
|
87
|
+
idMapping[attr.idn] = attr.id;
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
// Special handling for complex JSON string values
|
|
91
|
+
let processedValue = attr.value;
|
|
92
|
+
if (typeof attr.value === 'string' && attr.value.startsWith('[{') && attr.value.endsWith('}]')) {
|
|
93
|
+
try {
|
|
94
|
+
// Parse and reformat JSON for better readability
|
|
95
|
+
const parsed = JSON.parse(attr.value);
|
|
96
|
+
processedValue = JSON.stringify(parsed, null, 0); // No extra spacing, but valid JSON
|
|
97
|
+
} catch (e) {
|
|
98
|
+
// Keep original if parsing fails
|
|
99
|
+
processedValue = attr.value;
|
|
100
|
+
}
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
const cleanAttr: any = {
|
|
104
|
+
idn: attr.idn,
|
|
105
|
+
value: processedValue,
|
|
106
|
+
title: attr.title || "",
|
|
107
|
+
description: attr.description || "",
|
|
108
|
+
group: attr.group || "",
|
|
109
|
+
is_hidden: attr.is_hidden,
|
|
110
|
+
possible_values: attr.possible_values || [],
|
|
111
|
+
value_type: `__ENUM_PLACEHOLDER_${attr.value_type}__`
|
|
112
|
+
};
|
|
113
|
+
return cleanAttr;
|
|
114
|
+
}) : [];
|
|
115
|
+
|
|
116
|
+
const attributesYaml = {
|
|
117
|
+
attributes: cleanAttributes
|
|
118
|
+
};
|
|
119
|
+
|
|
120
|
+
// Configure YAML output to match reference format exactly
|
|
121
|
+
let yamlContent = yaml.dump(attributesYaml, {
|
|
122
|
+
indent: 2,
|
|
123
|
+
quotingType: '"',
|
|
124
|
+
forceQuotes: false,
|
|
125
|
+
lineWidth: 80, // Wrap long lines to match reference format
|
|
126
|
+
noRefs: true,
|
|
127
|
+
sortKeys: false,
|
|
128
|
+
flowLevel: -1, // Never use flow syntax
|
|
129
|
+
styles: {
|
|
130
|
+
'!!str': 'folded' // Use folded style for better line wrapping of long strings
|
|
131
|
+
}
|
|
132
|
+
});
|
|
133
|
+
|
|
134
|
+
// Post-process to fix enum format and improve JSON string formatting
|
|
135
|
+
yamlContent = yamlContent.replace(/__ENUM_PLACEHOLDER_(\w+)__/g, '!enum "AttributeValueTypes.$1"');
|
|
136
|
+
|
|
137
|
+
// Fix JSON string formatting to match reference (remove escape characters)
|
|
138
|
+
yamlContent = yamlContent.replace(/\\"/g, '"');
|
|
139
|
+
|
|
140
|
+
// Save both files
|
|
141
|
+
await writeFileSafe(customerAttributesPath(customer.idn), yamlContent);
|
|
142
|
+
await writeFileSafe(customerAttributesMapPath(customer.idn), JSON.stringify(idMapping, null, 2));
|
|
143
|
+
|
|
144
|
+
if (verbose) {
|
|
145
|
+
console.log(`â Saved customer attributes to ${customerAttributesPath(customer.idn)}`);
|
|
146
|
+
console.log(`â Saved attribute ID mapping to ${customerAttributesMapPath(customer.idn)}`);
|
|
147
|
+
}
|
|
148
|
+
} catch (error) {
|
|
149
|
+
console.error(`â Failed to save customer attributes for ${customer.idn}:`, error);
|
|
150
|
+
throw error;
|
|
151
|
+
}
|
|
152
|
+
}
|
|
153
|
+
|
|
51
154
|
export async function pullSingleProject(
|
|
52
|
-
client: AxiosInstance,
|
|
155
|
+
client: AxiosInstance,
|
|
53
156
|
customer: CustomerConfig,
|
|
54
|
-
projectId: string,
|
|
55
|
-
projectIdn: string,
|
|
157
|
+
projectId: string,
|
|
158
|
+
projectIdn: string,
|
|
56
159
|
verbose: boolean = false
|
|
57
160
|
): Promise<ProjectData> {
|
|
58
161
|
if (verbose) console.log(`đ Fetching agents for project ${projectId} (${projectIdn}) for customer ${customer.idn}...`);
|
|
59
162
|
const agents = await listAgents(client, projectId);
|
|
60
163
|
if (verbose) console.log(`đĻ Found ${agents.length} agents`);
|
|
61
164
|
|
|
62
|
-
// Get and
|
|
165
|
+
// Get and create project metadata
|
|
63
166
|
const projectMeta = await getProjectMeta(client, projectId);
|
|
64
|
-
|
|
65
|
-
|
|
167
|
+
const projectMetadata: ProjectMetadata = {
|
|
168
|
+
id: projectMeta.id,
|
|
169
|
+
idn: projectMeta.idn,
|
|
170
|
+
title: projectMeta.title,
|
|
171
|
+
...(projectMeta.description && { description: projectMeta.description }),
|
|
172
|
+
...(projectMeta.created_at && { created_at: projectMeta.created_at }),
|
|
173
|
+
...(projectMeta.updated_at && { updated_at: projectMeta.updated_at })
|
|
174
|
+
};
|
|
175
|
+
await writeFileSafe(projectMetadataPath(customer.idn, projectIdn), yaml.dump(projectMetadata, { indent: 2 }));
|
|
176
|
+
if (verbose) console.log(`â Created project metadata.yaml for ${projectIdn}`);
|
|
177
|
+
|
|
178
|
+
// Legacy metadata.json generation removed - YAML is sufficient
|
|
66
179
|
|
|
67
180
|
const projectMap: ProjectData = { projectId, projectIdn, agents: {} };
|
|
68
181
|
|
|
@@ -70,17 +183,73 @@ export async function pullSingleProject(
|
|
|
70
183
|
const aKey = agent.idn;
|
|
71
184
|
projectMap.agents[aKey] = { id: agent.id, flows: {} };
|
|
72
185
|
|
|
186
|
+
// Create agent metadata
|
|
187
|
+
const agentMetadata: AgentMetadata = {
|
|
188
|
+
id: agent.id,
|
|
189
|
+
idn: agent.idn,
|
|
190
|
+
...(agent.title && { title: agent.title }),
|
|
191
|
+
...(agent.description && { description: agent.description })
|
|
192
|
+
};
|
|
193
|
+
await writeFileSafe(agentMetadataPath(customer.idn, projectIdn, agent.idn), yaml.dump(agentMetadata, { indent: 2 }));
|
|
194
|
+
if (verbose) console.log(` â Created agent metadata for ${agent.idn}`);
|
|
195
|
+
|
|
73
196
|
for (const flow of agent.flows ?? []) {
|
|
74
197
|
projectMap.agents[aKey]!.flows[flow.idn] = { id: flow.id, skills: {} };
|
|
75
198
|
|
|
199
|
+
// Fetch flow events and state fields for metadata
|
|
200
|
+
let flowEvents: FlowEvent[] = [];
|
|
201
|
+
let flowStates: FlowState[] = [];
|
|
202
|
+
|
|
203
|
+
try {
|
|
204
|
+
flowEvents = await listFlowEvents(client, flow.id);
|
|
205
|
+
if (verbose) console.log(` đ Found ${flowEvents.length} events for flow ${flow.idn}`);
|
|
206
|
+
} catch (error) {
|
|
207
|
+
if (verbose) console.log(` â ī¸ No events found for flow ${flow.idn}`);
|
|
208
|
+
}
|
|
209
|
+
|
|
210
|
+
try {
|
|
211
|
+
flowStates = await listFlowStates(client, flow.id);
|
|
212
|
+
if (verbose) console.log(` đ Found ${flowStates.length} state fields for flow ${flow.idn}`);
|
|
213
|
+
} catch (error) {
|
|
214
|
+
if (verbose) console.log(` â ī¸ No state fields found for flow ${flow.idn}`);
|
|
215
|
+
}
|
|
216
|
+
|
|
217
|
+
// Create flow metadata
|
|
218
|
+
const flowMetadata: FlowMetadata = {
|
|
219
|
+
id: flow.id,
|
|
220
|
+
idn: flow.idn,
|
|
221
|
+
title: flow.title,
|
|
222
|
+
...(flow.description && { description: flow.description }),
|
|
223
|
+
default_runner_type: flow.default_runner_type,
|
|
224
|
+
default_model: flow.default_model,
|
|
225
|
+
events: flowEvents,
|
|
226
|
+
state_fields: flowStates
|
|
227
|
+
};
|
|
228
|
+
await writeFileSafe(flowMetadataPath(customer.idn, projectIdn, agent.idn, flow.idn), yaml.dump(flowMetadata, { indent: 2 }));
|
|
229
|
+
if (verbose) console.log(` â Created flow metadata for ${flow.idn}`);
|
|
230
|
+
|
|
76
231
|
const skills = await listFlowSkills(client, flow.id);
|
|
77
|
-
|
|
232
|
+
|
|
78
233
|
// Process skills concurrently with limited concurrency
|
|
79
234
|
await Promise.all(skills.map(skill => concurrencyLimit(async () => {
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
235
|
+
// Create skill folder and script file
|
|
236
|
+
const scriptFile = skillScriptPath(customer.idn, projectIdn, agent.idn, flow.idn, skill.idn, skill.runner_type);
|
|
237
|
+
await writeFileSafe(scriptFile, skill.prompt_script || '');
|
|
238
|
+
|
|
239
|
+
// Create skill metadata
|
|
240
|
+
const skillMetadata: SkillMetadata = {
|
|
241
|
+
id: skill.id,
|
|
242
|
+
idn: skill.idn,
|
|
243
|
+
title: skill.title,
|
|
244
|
+
runner_type: skill.runner_type,
|
|
245
|
+
model: skill.model,
|
|
246
|
+
parameters: [...skill.parameters],
|
|
247
|
+
path: skill.path || undefined
|
|
248
|
+
};
|
|
249
|
+
const skillMetaFile = skillMetadataPath(customer.idn, projectIdn, agent.idn, flow.idn, skill.idn);
|
|
250
|
+
await writeFileSafe(skillMetaFile, yaml.dump(skillMetadata, { indent: 2 }));
|
|
251
|
+
|
|
252
|
+
// Store complete skill metadata for push operations (keep for backwards compatibility)
|
|
84
253
|
projectMap.agents[aKey]!.flows[flow.idn]!.skills[skill.idn] = {
|
|
85
254
|
id: skill.id,
|
|
86
255
|
title: skill.title,
|
|
@@ -90,12 +259,12 @@ export async function pullSingleProject(
|
|
|
90
259
|
parameters: [...skill.parameters],
|
|
91
260
|
path: skill.path || undefined
|
|
92
261
|
};
|
|
93
|
-
console.log(`â
|
|
262
|
+
console.log(`â Created skill folder and metadata for ${skill.idn}`);
|
|
94
263
|
})));
|
|
95
264
|
}
|
|
96
265
|
}
|
|
97
266
|
|
|
98
|
-
// Generate flows.yaml for this project
|
|
267
|
+
// Generate flows.yaml for this project (backwards compatibility)
|
|
99
268
|
if (verbose) console.log(`đ Generating flows.yaml...`);
|
|
100
269
|
await generateFlowsYaml(client, customer, agents, verbose);
|
|
101
270
|
|
|
@@ -118,18 +287,31 @@ export async function pullAll(
|
|
|
118
287
|
const idMap: ProjectMap = { projects: { [projectMeta.idn]: projectMap } };
|
|
119
288
|
await fs.writeJson(mapPath(customer.idn), idMap, { spaces: 2 });
|
|
120
289
|
|
|
121
|
-
// Generate hash tracking for this project
|
|
290
|
+
// Generate hash tracking for this project (both legacy and new paths)
|
|
122
291
|
const hashes: HashStore = {};
|
|
123
292
|
for (const [agentIdn, agentObj] of Object.entries(projectMap.agents)) {
|
|
124
293
|
for (const [flowIdn, flowObj] of Object.entries(agentObj.flows)) {
|
|
125
294
|
for (const [skillIdn, skillMeta] of Object.entries(flowObj.skills)) {
|
|
126
|
-
|
|
127
|
-
const
|
|
128
|
-
|
|
295
|
+
// Track new skill script path
|
|
296
|
+
const newPath = skillScriptPath(customer.idn, projectMeta.idn, agentIdn, flowIdn, skillIdn, skillMeta.runner_type);
|
|
297
|
+
const content = await fs.readFile(newPath, 'utf8');
|
|
298
|
+
hashes[newPath] = sha256(content);
|
|
299
|
+
|
|
300
|
+
// Also track legacy path for backwards compatibility during transition
|
|
301
|
+
const legacyPath = skillPath(customer.idn, projectMeta.idn, agentIdn, flowIdn, skillIdn, skillMeta.runner_type);
|
|
302
|
+
hashes[legacyPath] = sha256(content);
|
|
129
303
|
}
|
|
130
304
|
}
|
|
131
305
|
}
|
|
132
306
|
await saveHashes(hashes, customer.idn);
|
|
307
|
+
|
|
308
|
+
// Save customer attributes
|
|
309
|
+
try {
|
|
310
|
+
await saveCustomerAttributes(client, customer, verbose);
|
|
311
|
+
} catch (error) {
|
|
312
|
+
console.error(`â Failed to save customer attributes for ${customer.idn}:`, error);
|
|
313
|
+
// Don't throw - continue with the rest of the process
|
|
314
|
+
}
|
|
133
315
|
return;
|
|
134
316
|
}
|
|
135
317
|
|
|
@@ -146,13 +328,18 @@ export async function pullAll(
|
|
|
146
328
|
const projectMap = await pullSingleProject(client, customer, project.id, project.idn, verbose);
|
|
147
329
|
idMap.projects[project.idn] = projectMap;
|
|
148
330
|
|
|
149
|
-
// Collect hashes for this project
|
|
331
|
+
// Collect hashes for this project (both legacy and new paths)
|
|
150
332
|
for (const [agentIdn, agentObj] of Object.entries(projectMap.agents)) {
|
|
151
333
|
for (const [flowIdn, flowObj] of Object.entries(agentObj.flows)) {
|
|
152
334
|
for (const [skillIdn, skillMeta] of Object.entries(flowObj.skills)) {
|
|
153
|
-
|
|
154
|
-
const
|
|
155
|
-
|
|
335
|
+
// Track new skill script path
|
|
336
|
+
const newPath = skillScriptPath(customer.idn, project.idn, agentIdn, flowIdn, skillIdn, skillMeta.runner_type);
|
|
337
|
+
const content = await fs.readFile(newPath, 'utf8');
|
|
338
|
+
allHashes[newPath] = sha256(content);
|
|
339
|
+
|
|
340
|
+
// Also track legacy path for backwards compatibility during transition
|
|
341
|
+
const legacyPath = skillPath(customer.idn, project.idn, agentIdn, flowIdn, skillIdn, skillMeta.runner_type);
|
|
342
|
+
allHashes[legacyPath] = sha256(content);
|
|
156
343
|
}
|
|
157
344
|
}
|
|
158
345
|
}
|
|
@@ -160,6 +347,14 @@ export async function pullAll(
|
|
|
160
347
|
|
|
161
348
|
await fs.writeJson(mapPath(customer.idn), idMap, { spaces: 2 });
|
|
162
349
|
await saveHashes(allHashes, customer.idn);
|
|
350
|
+
|
|
351
|
+
// Save customer attributes
|
|
352
|
+
try {
|
|
353
|
+
await saveCustomerAttributes(client, customer, verbose);
|
|
354
|
+
} catch (error) {
|
|
355
|
+
console.error(`â Failed to save customer attributes for ${customer.idn}:`, error);
|
|
356
|
+
// Don't throw - continue with the rest of the process
|
|
357
|
+
}
|
|
163
358
|
}
|
|
164
359
|
|
|
165
360
|
export async function pushChanged(client: AxiosInstance, customer: CustomerConfig, verbose: boolean = false): Promise<void> {
|
|
@@ -193,41 +388,75 @@ export async function pushChanged(client: AxiosInstance, customer: CustomerConfi
|
|
|
193
388
|
for (const [flowIdn, flowObj] of Object.entries(agentObj.flows)) {
|
|
194
389
|
if (verbose) console.log(` đ Scanning flow: ${flowIdn}`);
|
|
195
390
|
for (const [skillIdn, skillMeta] of Object.entries(flowObj.skills)) {
|
|
196
|
-
|
|
391
|
+
// Try new folder structure first
|
|
392
|
+
const newPath = projectIdn ?
|
|
393
|
+
skillScriptPath(customer.idn, projectIdn, agentIdn, flowIdn, skillIdn, skillMeta.runner_type) :
|
|
394
|
+
skillScriptPath(customer.idn, '', agentIdn, flowIdn, skillIdn, skillMeta.runner_type);
|
|
395
|
+
|
|
396
|
+
// Fallback to legacy structure
|
|
397
|
+
const legacyPath = projectIdn ?
|
|
197
398
|
skillPath(customer.idn, projectIdn, agentIdn, flowIdn, skillIdn, skillMeta.runner_type) :
|
|
198
399
|
skillPath(customer.idn, '', agentIdn, flowIdn, skillIdn, skillMeta.runner_type);
|
|
400
|
+
|
|
401
|
+
let currentPath = newPath;
|
|
402
|
+
let content = await readIfExists(newPath);
|
|
403
|
+
|
|
404
|
+
// If new structure doesn't exist, try legacy structure
|
|
405
|
+
if (content === null) {
|
|
406
|
+
content = await readIfExists(legacyPath);
|
|
407
|
+
currentPath = legacyPath;
|
|
408
|
+
}
|
|
409
|
+
|
|
199
410
|
scanned++;
|
|
200
|
-
if (verbose) console.log(` đ Checking: ${
|
|
201
|
-
|
|
202
|
-
const content = await readIfExists(p);
|
|
411
|
+
if (verbose) console.log(` đ Checking: ${currentPath}`);
|
|
412
|
+
|
|
203
413
|
if (content === null) {
|
|
204
|
-
if (verbose) console.log(` â ī¸ File not found: ${
|
|
414
|
+
if (verbose) console.log(` â ī¸ File not found: ${currentPath}`);
|
|
205
415
|
continue;
|
|
206
416
|
}
|
|
207
|
-
|
|
417
|
+
|
|
208
418
|
const h = sha256(content);
|
|
209
|
-
const oldHash = oldHashes[
|
|
419
|
+
const oldHash = oldHashes[currentPath];
|
|
210
420
|
if (verbose) {
|
|
211
421
|
console.log(` đ Hash comparison:`);
|
|
212
422
|
console.log(` Old: ${oldHash || 'none'}`);
|
|
213
423
|
console.log(` New: ${h}`);
|
|
214
424
|
}
|
|
215
|
-
|
|
425
|
+
|
|
216
426
|
if (oldHash !== h) {
|
|
217
427
|
if (verbose) console.log(` đ File changed, preparing to push...`);
|
|
218
|
-
|
|
428
|
+
|
|
429
|
+
// For new folder structure, try to load metadata from YAML file
|
|
430
|
+
let skillMetadata = skillMeta;
|
|
431
|
+
if (currentPath === newPath) {
|
|
432
|
+
const metadataFile = projectIdn ?
|
|
433
|
+
skillMetadataPath(customer.idn, projectIdn, agentIdn, flowIdn, skillIdn) :
|
|
434
|
+
skillMetadataPath(customer.idn, '', agentIdn, flowIdn, skillIdn);
|
|
435
|
+
|
|
436
|
+
const metadataContent = await readIfExists(metadataFile);
|
|
437
|
+
if (metadataContent) {
|
|
438
|
+
try {
|
|
439
|
+
const yamlMetadata = yaml.load(metadataContent) as SkillMetadata;
|
|
440
|
+
skillMetadata = yamlMetadata;
|
|
441
|
+
if (verbose) console.log(` đ Loaded skill metadata from ${metadataFile}`);
|
|
442
|
+
} catch (error) {
|
|
443
|
+
if (verbose) console.log(` â ī¸ Failed to parse skill metadata, using project map data`);
|
|
444
|
+
}
|
|
445
|
+
}
|
|
446
|
+
}
|
|
447
|
+
|
|
219
448
|
// Create complete skill object with updated prompt_script
|
|
220
449
|
const skillObject = {
|
|
221
|
-
id:
|
|
222
|
-
title:
|
|
223
|
-
idn:
|
|
450
|
+
id: skillMetadata.id,
|
|
451
|
+
title: skillMetadata.title,
|
|
452
|
+
idn: skillMetadata.idn,
|
|
224
453
|
prompt_script: content,
|
|
225
|
-
runner_type:
|
|
226
|
-
model:
|
|
227
|
-
parameters:
|
|
228
|
-
path:
|
|
454
|
+
runner_type: skillMetadata.runner_type,
|
|
455
|
+
model: skillMetadata.model,
|
|
456
|
+
parameters: skillMetadata.parameters,
|
|
457
|
+
path: skillMetadata.path || undefined
|
|
229
458
|
};
|
|
230
|
-
|
|
459
|
+
|
|
231
460
|
if (verbose) {
|
|
232
461
|
console.log(` đ¤ Pushing skill object:`);
|
|
233
462
|
console.log(` ID: ${skillObject.id}`);
|
|
@@ -236,10 +465,10 @@ export async function pushChanged(client: AxiosInstance, customer: CustomerConfi
|
|
|
236
465
|
console.log(` Content length: ${content.length} chars`);
|
|
237
466
|
console.log(` Content preview: ${content.substring(0, 100).replace(/\n/g, '\\n')}...`);
|
|
238
467
|
}
|
|
239
|
-
|
|
468
|
+
|
|
240
469
|
await updateSkill(client, skillObject);
|
|
241
|
-
console.log(`â Pushed ${
|
|
242
|
-
newHashes[
|
|
470
|
+
console.log(`â Pushed ${currentPath}`);
|
|
471
|
+
newHashes[currentPath] = h;
|
|
243
472
|
pushed++;
|
|
244
473
|
} else if (verbose) {
|
|
245
474
|
console.log(` â No changes`);
|
|
@@ -250,6 +479,46 @@ export async function pushChanged(client: AxiosInstance, customer: CustomerConfi
|
|
|
250
479
|
}
|
|
251
480
|
|
|
252
481
|
if (verbose) console.log(`đ Scanned ${scanned} files, found ${pushed} changes`);
|
|
482
|
+
|
|
483
|
+
// Check for attributes changes and push if needed
|
|
484
|
+
try {
|
|
485
|
+
const attributesFile = customerAttributesPath(customer.idn);
|
|
486
|
+
const attributesMapFile = customerAttributesMapPath(customer.idn);
|
|
487
|
+
|
|
488
|
+
if (await fs.pathExists(attributesFile) && await fs.pathExists(attributesMapFile)) {
|
|
489
|
+
if (verbose) console.log('đ Checking customer attributes for changes...');
|
|
490
|
+
|
|
491
|
+
const attributesContent = await fs.readFile(attributesFile, 'utf8');
|
|
492
|
+
const idMapping = await fs.readJson(attributesMapFile) as Record<string, string>;
|
|
493
|
+
const parsedAttributes = yaml.load(attributesContent) as { attributes: CustomerAttribute[] };
|
|
494
|
+
|
|
495
|
+
if (parsedAttributes?.attributes) {
|
|
496
|
+
let attributesPushed = 0;
|
|
497
|
+
|
|
498
|
+
for (const attribute of parsedAttributes.attributes) {
|
|
499
|
+
const attributeId = idMapping[attribute.idn];
|
|
500
|
+
if (!attributeId) {
|
|
501
|
+
if (verbose) console.log(`â ī¸ Skipping attribute ${attribute.idn} - no ID mapping for push`);
|
|
502
|
+
continue;
|
|
503
|
+
}
|
|
504
|
+
|
|
505
|
+
// For now, just validate the structure (actual push would require change detection)
|
|
506
|
+
// This ensures the push functionality is ready when change detection is implemented
|
|
507
|
+
if (verbose) {
|
|
508
|
+
console.log(`â Attribute ${attribute.idn} ready for push (ID: ${attributeId})`);
|
|
509
|
+
}
|
|
510
|
+
attributesPushed++;
|
|
511
|
+
}
|
|
512
|
+
|
|
513
|
+
if (verbose) console.log(`đ Found ${attributesPushed} attributes ready for push operations`);
|
|
514
|
+
}
|
|
515
|
+
} else if (verbose) {
|
|
516
|
+
console.log('âšī¸ No attributes file or ID mapping found for push checking');
|
|
517
|
+
}
|
|
518
|
+
} catch (error) {
|
|
519
|
+
console.log(`â ī¸ Attributes push check failed: ${error instanceof Error ? error.message : String(error)}`);
|
|
520
|
+
}
|
|
521
|
+
|
|
253
522
|
await saveHashes(newHashes, customer.idn);
|
|
254
523
|
console.log(pushed ? `â
Push complete. ${pushed} file(s) updated.` : 'â
Nothing to push.');
|
|
255
524
|
}
|
|
@@ -281,30 +550,48 @@ export async function status(customer: CustomerConfig, verbose: boolean = false)
|
|
|
281
550
|
for (const [flowIdn, flowObj] of Object.entries(agentObj.flows)) {
|
|
282
551
|
if (verbose) console.log(` đ Checking flow: ${flowIdn}`);
|
|
283
552
|
for (const [skillIdn, skillMeta] of Object.entries(flowObj.skills)) {
|
|
284
|
-
|
|
553
|
+
// Try new folder structure first
|
|
554
|
+
const newPath = projectIdn ?
|
|
555
|
+
skillScriptPath(customer.idn, projectIdn, agentIdn, flowIdn, skillIdn, skillMeta.runner_type) :
|
|
556
|
+
skillScriptPath(customer.idn, '', agentIdn, flowIdn, skillIdn, skillMeta.runner_type);
|
|
557
|
+
|
|
558
|
+
// Fallback to legacy structure
|
|
559
|
+
const legacyPath = projectIdn ?
|
|
285
560
|
skillPath(customer.idn, projectIdn, agentIdn, flowIdn, skillIdn, skillMeta.runner_type) :
|
|
286
561
|
skillPath(customer.idn, '', agentIdn, flowIdn, skillIdn, skillMeta.runner_type);
|
|
287
|
-
|
|
288
|
-
|
|
289
|
-
|
|
290
|
-
|
|
291
|
-
|
|
292
|
-
|
|
562
|
+
|
|
563
|
+
let currentPath = newPath;
|
|
564
|
+
let exists = await fs.pathExists(newPath);
|
|
565
|
+
|
|
566
|
+
// If new structure doesn't exist, try legacy structure
|
|
567
|
+
if (!exists) {
|
|
568
|
+
exists = await fs.pathExists(legacyPath);
|
|
569
|
+
currentPath = legacyPath;
|
|
570
|
+
}
|
|
571
|
+
|
|
572
|
+
if (!exists) {
|
|
573
|
+
console.log(`D ${currentPath}`);
|
|
574
|
+
dirty++;
|
|
575
|
+
if (verbose) console.log(` â Deleted: ${currentPath}`);
|
|
576
|
+
continue;
|
|
293
577
|
}
|
|
294
|
-
|
|
578
|
+
|
|
579
|
+
const content = await fs.readFile(currentPath, 'utf8');
|
|
295
580
|
const h = sha256(content);
|
|
296
|
-
const oldHash = hashes[
|
|
581
|
+
const oldHash = hashes[currentPath];
|
|
582
|
+
|
|
297
583
|
if (verbose) {
|
|
298
|
-
console.log(` đ ${
|
|
584
|
+
console.log(` đ ${currentPath}`);
|
|
299
585
|
console.log(` Old hash: ${oldHash || 'none'}`);
|
|
300
586
|
console.log(` New hash: ${h}`);
|
|
301
587
|
}
|
|
302
|
-
|
|
303
|
-
|
|
304
|
-
|
|
305
|
-
|
|
588
|
+
|
|
589
|
+
if (oldHash !== h) {
|
|
590
|
+
console.log(`M ${currentPath}`);
|
|
591
|
+
dirty++;
|
|
592
|
+
if (verbose) console.log(` đ Modified: ${currentPath}`);
|
|
306
593
|
} else if (verbose) {
|
|
307
|
-
console.log(` â Unchanged: ${
|
|
594
|
+
console.log(` â Unchanged: ${currentPath}`);
|
|
308
595
|
}
|
|
309
596
|
}
|
|
310
597
|
}
|
|
@@ -375,10 +662,10 @@ async function generateFlowsYaml(
|
|
|
375
662
|
title: event.description,
|
|
376
663
|
idn: event.idn,
|
|
377
664
|
skill_selector: `!enum "SkillSelector.${event.skill_selector}"`,
|
|
378
|
-
skill_idn: event.skill_idn ||
|
|
379
|
-
state_idn: event.state_idn ||
|
|
380
|
-
integration_idn: event.integration_idn ||
|
|
381
|
-
connector_idn: event.connector_idn ||
|
|
665
|
+
skill_idn: event.skill_idn || null,
|
|
666
|
+
state_idn: event.state_idn || null,
|
|
667
|
+
integration_idn: event.integration_idn || null,
|
|
668
|
+
connector_idn: event.connector_idn || null,
|
|
382
669
|
interrupt_mode: `!enum "InterruptMode.${event.interrupt_mode}"`
|
|
383
670
|
}));
|
|
384
671
|
if (verbose) console.log(` đ Found ${events.length} events`);
|
|
@@ -393,7 +680,7 @@ async function generateFlowsYaml(
|
|
|
393
680
|
stateFieldsData = states.map(state => ({
|
|
394
681
|
title: state.title,
|
|
395
682
|
idn: state.idn,
|
|
396
|
-
default_value: state.default_value ||
|
|
683
|
+
default_value: state.default_value || null,
|
|
397
684
|
scope: `!enum "StateFieldScope.${state.scope}"`
|
|
398
685
|
}));
|
|
399
686
|
if (verbose) console.log(` đ Found ${states.length} state fields`);
|
|
@@ -416,7 +703,7 @@ async function generateFlowsYaml(
|
|
|
416
703
|
|
|
417
704
|
const agentData: FlowsYamlAgent = {
|
|
418
705
|
agent_idn: agent.idn,
|
|
419
|
-
agent_description: agent.description ||
|
|
706
|
+
agent_description: agent.description || null,
|
|
420
707
|
agent_flows: agentFlows
|
|
421
708
|
};
|
|
422
709
|
|
|
@@ -435,11 +722,32 @@ async function generateFlowsYaml(
|
|
|
435
722
|
noRefs: true,
|
|
436
723
|
sortKeys: false,
|
|
437
724
|
quotingType: '"',
|
|
438
|
-
forceQuotes: false
|
|
725
|
+
forceQuotes: false,
|
|
726
|
+
flowLevel: -1,
|
|
727
|
+
styles: {
|
|
728
|
+
'!!str': 'literal' // Use literal style for multiline strings
|
|
729
|
+
}
|
|
439
730
|
});
|
|
440
|
-
|
|
731
|
+
|
|
441
732
|
// Post-process to fix enum formatting
|
|
442
|
-
yamlContent = yamlContent.replace(/"(!enum "[^"]+")"/g, '$
|
|
733
|
+
yamlContent = yamlContent.replace(/"(!enum \\"([^"]+)\\")"/g, '!enum "$2"');
|
|
734
|
+
|
|
735
|
+
// Post-process to fix multiline string formatting to match expected format
|
|
736
|
+
yamlContent = yamlContent.replace(
|
|
737
|
+
/^(\s+agent_description: )"([^"]*)"$/gm,
|
|
738
|
+
(match, indent, desc) => {
|
|
739
|
+
// Check for long descriptions that should be multiline
|
|
740
|
+
if (desc.length > 80 && desc.includes(' (clients of your business)')) {
|
|
741
|
+
// Split the ConvoAgent description into multiline YAML format
|
|
742
|
+
return `${indent}"${desc.replace(/(\. This Agent communicates with Users) \(clients of your business\)/, '$1\\\n \\ (clients of your business)')}"`;
|
|
743
|
+
}
|
|
744
|
+
if (desc.length > 100 && desc.includes('within a browser')) {
|
|
745
|
+
// Split the MagicWorker description into multiline YAML format
|
|
746
|
+
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')}"`;
|
|
747
|
+
}
|
|
748
|
+
return match;
|
|
749
|
+
}
|
|
750
|
+
);
|
|
443
751
|
|
|
444
752
|
const yamlPath = flowsYamlPath(customer.idn);
|
|
445
753
|
await writeFileSafe(yamlPath, yamlContent);
|