newo 3.7.1 → 3.7.2
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +12 -0
- package/dist/api.d.ts +33 -1
- package/dist/api.js +22 -0
- package/dist/domain/strategies/sync/ProjectSyncStrategy.d.ts +7 -0
- package/dist/domain/strategies/sync/ProjectSyncStrategy.js +81 -1
- package/dist/domain/strategies/sync/V2ProjectSyncStrategy.d.ts +11 -0
- package/dist/domain/strategies/sync/V2ProjectSyncStrategy.js +125 -2
- package/dist/sync/flow-metadata.d.ts +67 -0
- package/dist/sync/flow-metadata.js +283 -0
- package/dist/sync/push.js +65 -2
- package/dist/types.d.ts +37 -0
- package/package.json +1 -1
- package/src/api.ts +61 -0
- package/src/domain/strategies/sync/ProjectSyncStrategy.ts +100 -0
- package/src/domain/strategies/sync/V2ProjectSyncStrategy.ts +147 -0
- package/src/sync/flow-metadata.ts +345 -0
- package/src/sync/push.ts +75 -2
- package/src/types.ts +40 -0
|
@@ -0,0 +1,283 @@
|
|
|
1
|
+
import { listFlowEvents, listFlowStates, createFlowEvent, updateFlowEvent, deleteFlowEvent, createFlowState, updateFlowState, deleteFlowState, updateFlow } from '../api.js';
|
|
2
|
+
export function emptyFlowSyncCounts() {
|
|
3
|
+
return {
|
|
4
|
+
flowsUpdated: 0,
|
|
5
|
+
eventsCreated: 0,
|
|
6
|
+
eventsUpdated: 0,
|
|
7
|
+
eventsDeleted: 0,
|
|
8
|
+
statesCreated: 0,
|
|
9
|
+
statesUpdated: 0,
|
|
10
|
+
statesDeleted: 0,
|
|
11
|
+
errors: []
|
|
12
|
+
};
|
|
13
|
+
}
|
|
14
|
+
/**
|
|
15
|
+
* True when remote FlowEvent fields differ from what the local metadata says.
|
|
16
|
+
* We only compare semantic fields the platform stores - `id` is platform-owned.
|
|
17
|
+
*/
|
|
18
|
+
export function flowEventDiffers(local, remote) {
|
|
19
|
+
return (normalizeStr(local.description) !== normalizeStr(remote.description) ||
|
|
20
|
+
normalizeStr(local.skill_selector) !== normalizeStr(remote.skill_selector) ||
|
|
21
|
+
normalizeStr(local.skill_idn) !== normalizeStr(remote.skill_idn) ||
|
|
22
|
+
normalizeStr(local.state_idn) !== normalizeStr(remote.state_idn) ||
|
|
23
|
+
normalizeStr(local.interrupt_mode) !== normalizeStr(remote.interrupt_mode) ||
|
|
24
|
+
normalizeStr(local.integration_idn) !== normalizeStr(remote.integration_idn) ||
|
|
25
|
+
normalizeStr(local.connector_idn) !== normalizeStr(remote.connector_idn));
|
|
26
|
+
}
|
|
27
|
+
export function flowStateDiffers(local, remote) {
|
|
28
|
+
return (normalizeStr(local.title) !== normalizeStr(remote.title) ||
|
|
29
|
+
normalizeStr(local.default_value) !== normalizeStr(remote.default_value) ||
|
|
30
|
+
normalizeStr(local.scope) !== normalizeStr(remote.scope));
|
|
31
|
+
}
|
|
32
|
+
/**
|
|
33
|
+
* Normalizes nullable/undefined string fields so YAML round-trips don't
|
|
34
|
+
* register as differences. `null`, `undefined`, and missing all collapse to ''.
|
|
35
|
+
*/
|
|
36
|
+
function normalizeStr(value) {
|
|
37
|
+
if (value === null || value === undefined)
|
|
38
|
+
return '';
|
|
39
|
+
return String(value);
|
|
40
|
+
}
|
|
41
|
+
function buildEventCreateRequest(local) {
|
|
42
|
+
return {
|
|
43
|
+
idn: local.idn,
|
|
44
|
+
description: local.description ?? '',
|
|
45
|
+
skill_selector: local.skill_selector,
|
|
46
|
+
...(local.skill_idn != null ? { skill_idn: local.skill_idn } : {}),
|
|
47
|
+
state_idn: local.state_idn ?? null,
|
|
48
|
+
interrupt_mode: local.interrupt_mode,
|
|
49
|
+
integration_idn: local.integration_idn ?? '',
|
|
50
|
+
connector_idn: local.connector_idn ?? ''
|
|
51
|
+
};
|
|
52
|
+
}
|
|
53
|
+
function buildEventUpdateRequest(local) {
|
|
54
|
+
return {
|
|
55
|
+
idn: local.idn,
|
|
56
|
+
description: local.description ?? '',
|
|
57
|
+
skill_selector: local.skill_selector,
|
|
58
|
+
skill_idn: local.skill_idn ?? null,
|
|
59
|
+
state_idn: local.state_idn ?? null,
|
|
60
|
+
interrupt_mode: local.interrupt_mode,
|
|
61
|
+
integration_idn: local.integration_idn ?? null,
|
|
62
|
+
connector_idn: local.connector_idn ?? null
|
|
63
|
+
};
|
|
64
|
+
}
|
|
65
|
+
function buildStateCreateRequest(local) {
|
|
66
|
+
const req = {
|
|
67
|
+
title: local.title || local.idn,
|
|
68
|
+
idn: local.idn,
|
|
69
|
+
scope: local.scope
|
|
70
|
+
};
|
|
71
|
+
if (local.default_value != null) {
|
|
72
|
+
req.default_value = local.default_value;
|
|
73
|
+
}
|
|
74
|
+
return req;
|
|
75
|
+
}
|
|
76
|
+
function buildStateUpdateRequest(local) {
|
|
77
|
+
const req = {
|
|
78
|
+
title: local.title || local.idn,
|
|
79
|
+
idn: local.idn,
|
|
80
|
+
scope: local.scope
|
|
81
|
+
};
|
|
82
|
+
if (local.default_value != null) {
|
|
83
|
+
req.default_value = local.default_value;
|
|
84
|
+
}
|
|
85
|
+
return req;
|
|
86
|
+
}
|
|
87
|
+
function shouldUpdateFlow(local, remote) {
|
|
88
|
+
return (normalizeStr(local.title) !== normalizeStr(remote.title) ||
|
|
89
|
+
normalizeStr(local.description) !== normalizeStr(remote.description) ||
|
|
90
|
+
normalizeStr(local.default_runner_type) !== normalizeStr(remote.default_runner_type));
|
|
91
|
+
}
|
|
92
|
+
/**
|
|
93
|
+
* Reconcile one flow's metadata with the platform.
|
|
94
|
+
*
|
|
95
|
+
* @param client authenticated Axios client
|
|
96
|
+
* @param flowId platform flow ID (UUID)
|
|
97
|
+
* @param local parsed FlowMetadata from the customer's local YAML
|
|
98
|
+
* @param remoteFlow flow data fetched from GET /flows/{id} - if null,
|
|
99
|
+
* flow-level updates are skipped (still syncs children).
|
|
100
|
+
* Pass null when caller already knows the GET endpoint
|
|
101
|
+
* will 404 (e.g. legacy data) or wants children-only.
|
|
102
|
+
* @param verbose when true, emits per-operation log lines
|
|
103
|
+
* @param counts shared counter mutated in place across multiple flows
|
|
104
|
+
*/
|
|
105
|
+
export async function syncFlowMetadata(client, flowId, local, remoteFlow, verbose, counts) {
|
|
106
|
+
// 1. Flow-level fields (title, description, runner type)
|
|
107
|
+
if (remoteFlow && shouldUpdateFlow(local, remoteFlow)) {
|
|
108
|
+
try {
|
|
109
|
+
const updateRequest = {
|
|
110
|
+
idn: local.idn,
|
|
111
|
+
title: local.title,
|
|
112
|
+
description: local.description ?? '',
|
|
113
|
+
default_runner_type: local.default_runner_type,
|
|
114
|
+
default_model: local.default_model
|
|
115
|
+
};
|
|
116
|
+
await updateFlow(client, flowId, updateRequest);
|
|
117
|
+
counts.flowsUpdated++;
|
|
118
|
+
if (verbose) {
|
|
119
|
+
console.log(` ↑ Updated flow metadata: ${local.idn} (title: "${remoteFlow.title}" → "${local.title}")`);
|
|
120
|
+
}
|
|
121
|
+
}
|
|
122
|
+
catch (error) {
|
|
123
|
+
const msg = error instanceof Error ? error.message : String(error);
|
|
124
|
+
counts.errors.push(`Failed to update flow ${local.idn}: ${msg}`);
|
|
125
|
+
console.error(` ❌ Failed to update flow ${local.idn}: ${msg}`);
|
|
126
|
+
}
|
|
127
|
+
}
|
|
128
|
+
// 2. Events
|
|
129
|
+
let remoteEvents = [];
|
|
130
|
+
try {
|
|
131
|
+
remoteEvents = await listFlowEvents(client, flowId);
|
|
132
|
+
}
|
|
133
|
+
catch (error) {
|
|
134
|
+
const msg = error instanceof Error ? error.message : String(error);
|
|
135
|
+
counts.errors.push(`Failed to list events for flow ${local.idn}: ${msg}`);
|
|
136
|
+
return;
|
|
137
|
+
}
|
|
138
|
+
const localEvents = local.events ?? [];
|
|
139
|
+
const remoteByIdn = new Map(remoteEvents.map(e => [e.idn, e]));
|
|
140
|
+
const localByIdn = new Map(localEvents.map(e => [e.idn, e]));
|
|
141
|
+
// Create or update events present locally
|
|
142
|
+
for (const localEvent of localEvents) {
|
|
143
|
+
const remote = remoteByIdn.get(localEvent.idn);
|
|
144
|
+
if (!remote) {
|
|
145
|
+
// Create
|
|
146
|
+
try {
|
|
147
|
+
await createFlowEvent(client, flowId, buildEventCreateRequest(localEvent));
|
|
148
|
+
counts.eventsCreated++;
|
|
149
|
+
if (verbose)
|
|
150
|
+
console.log(` ↑ Created event: ${local.idn}/${localEvent.idn}`);
|
|
151
|
+
}
|
|
152
|
+
catch (error) {
|
|
153
|
+
const msg = error instanceof Error ? error.message : String(error);
|
|
154
|
+
counts.errors.push(`Failed to create event ${localEvent.idn} in flow ${local.idn}: ${msg}`);
|
|
155
|
+
console.error(` ❌ Failed to create event ${localEvent.idn}: ${msg}`);
|
|
156
|
+
}
|
|
157
|
+
}
|
|
158
|
+
else if (flowEventDiffers(localEvent, remote)) {
|
|
159
|
+
try {
|
|
160
|
+
await updateFlowEvent(client, remote.id, buildEventUpdateRequest(localEvent));
|
|
161
|
+
counts.eventsUpdated++;
|
|
162
|
+
if (verbose)
|
|
163
|
+
console.log(` ↑ Updated event: ${local.idn}/${localEvent.idn}`);
|
|
164
|
+
}
|
|
165
|
+
catch (error) {
|
|
166
|
+
const msg = error instanceof Error ? error.message : String(error);
|
|
167
|
+
counts.errors.push(`Failed to update event ${localEvent.idn} in flow ${local.idn}: ${msg}`);
|
|
168
|
+
console.error(` ❌ Failed to update event ${localEvent.idn}: ${msg}`);
|
|
169
|
+
}
|
|
170
|
+
}
|
|
171
|
+
}
|
|
172
|
+
// Delete events present remotely but missing locally
|
|
173
|
+
for (const remoteEvent of remoteEvents) {
|
|
174
|
+
if (!localByIdn.has(remoteEvent.idn)) {
|
|
175
|
+
try {
|
|
176
|
+
await deleteFlowEvent(client, remoteEvent.id);
|
|
177
|
+
counts.eventsDeleted++;
|
|
178
|
+
if (verbose)
|
|
179
|
+
console.log(` ↑ Deleted event: ${local.idn}/${remoteEvent.idn}`);
|
|
180
|
+
}
|
|
181
|
+
catch (error) {
|
|
182
|
+
const msg = error instanceof Error ? error.message : String(error);
|
|
183
|
+
counts.errors.push(`Failed to delete event ${remoteEvent.idn} in flow ${local.idn}: ${msg}`);
|
|
184
|
+
console.error(` ❌ Failed to delete event ${remoteEvent.idn}: ${msg}`);
|
|
185
|
+
}
|
|
186
|
+
}
|
|
187
|
+
}
|
|
188
|
+
// 3. State fields
|
|
189
|
+
let remoteStates = [];
|
|
190
|
+
try {
|
|
191
|
+
remoteStates = await listFlowStates(client, flowId);
|
|
192
|
+
}
|
|
193
|
+
catch (error) {
|
|
194
|
+
const msg = error instanceof Error ? error.message : String(error);
|
|
195
|
+
counts.errors.push(`Failed to list states for flow ${local.idn}: ${msg}`);
|
|
196
|
+
return;
|
|
197
|
+
}
|
|
198
|
+
const localStates = local.state_fields ?? [];
|
|
199
|
+
const remoteStatesByIdn = new Map(remoteStates.map(s => [s.idn, s]));
|
|
200
|
+
const localStatesByIdn = new Map(localStates.map(s => [s.idn, s]));
|
|
201
|
+
for (const localState of localStates) {
|
|
202
|
+
const remote = remoteStatesByIdn.get(localState.idn);
|
|
203
|
+
if (!remote) {
|
|
204
|
+
try {
|
|
205
|
+
await createFlowState(client, flowId, buildStateCreateRequest(localState));
|
|
206
|
+
counts.statesCreated++;
|
|
207
|
+
if (verbose)
|
|
208
|
+
console.log(` ↑ Created state: ${local.idn}/${localState.idn}`);
|
|
209
|
+
}
|
|
210
|
+
catch (error) {
|
|
211
|
+
const msg = error instanceof Error ? error.message : String(error);
|
|
212
|
+
counts.errors.push(`Failed to create state ${localState.idn} in flow ${local.idn}: ${msg}`);
|
|
213
|
+
console.error(` ❌ Failed to create state ${localState.idn}: ${msg}`);
|
|
214
|
+
}
|
|
215
|
+
}
|
|
216
|
+
else if (flowStateDiffers(localState, remote)) {
|
|
217
|
+
try {
|
|
218
|
+
await updateFlowState(client, remote.id, buildStateUpdateRequest(localState));
|
|
219
|
+
counts.statesUpdated++;
|
|
220
|
+
if (verbose)
|
|
221
|
+
console.log(` ↑ Updated state: ${local.idn}/${localState.idn}`);
|
|
222
|
+
}
|
|
223
|
+
catch (error) {
|
|
224
|
+
const msg = error instanceof Error ? error.message : String(error);
|
|
225
|
+
counts.errors.push(`Failed to update state ${localState.idn} in flow ${local.idn}: ${msg}`);
|
|
226
|
+
console.error(` ❌ Failed to update state ${localState.idn}: ${msg}`);
|
|
227
|
+
}
|
|
228
|
+
}
|
|
229
|
+
}
|
|
230
|
+
for (const remoteState of remoteStates) {
|
|
231
|
+
if (!localStatesByIdn.has(remoteState.idn)) {
|
|
232
|
+
try {
|
|
233
|
+
await deleteFlowState(client, remoteState.id);
|
|
234
|
+
counts.statesDeleted++;
|
|
235
|
+
if (verbose)
|
|
236
|
+
console.log(` ↑ Deleted state: ${local.idn}/${remoteState.idn}`);
|
|
237
|
+
}
|
|
238
|
+
catch (error) {
|
|
239
|
+
const msg = error instanceof Error ? error.message : String(error);
|
|
240
|
+
counts.errors.push(`Failed to delete state ${remoteState.idn} in flow ${local.idn}: ${msg}`);
|
|
241
|
+
console.error(` ❌ Failed to delete state ${remoteState.idn}: ${msg}`);
|
|
242
|
+
}
|
|
243
|
+
}
|
|
244
|
+
}
|
|
245
|
+
}
|
|
246
|
+
/**
|
|
247
|
+
* Combined count of operations across all categories.
|
|
248
|
+
*/
|
|
249
|
+
export function totalFlowSyncOps(counts) {
|
|
250
|
+
return (counts.flowsUpdated +
|
|
251
|
+
counts.eventsCreated + counts.eventsUpdated + counts.eventsDeleted +
|
|
252
|
+
counts.statesCreated + counts.statesUpdated + counts.statesDeleted);
|
|
253
|
+
}
|
|
254
|
+
/**
|
|
255
|
+
* Human-readable summary line for the push report.
|
|
256
|
+
*/
|
|
257
|
+
export function describeFlowSyncCounts(counts) {
|
|
258
|
+
const parts = [];
|
|
259
|
+
if (counts.flowsUpdated)
|
|
260
|
+
parts.push(`${counts.flowsUpdated} flow(s)`);
|
|
261
|
+
if (counts.eventsCreated || counts.eventsUpdated || counts.eventsDeleted) {
|
|
262
|
+
const eventOps = [];
|
|
263
|
+
if (counts.eventsCreated)
|
|
264
|
+
eventOps.push(`+${counts.eventsCreated}`);
|
|
265
|
+
if (counts.eventsUpdated)
|
|
266
|
+
eventOps.push(`~${counts.eventsUpdated}`);
|
|
267
|
+
if (counts.eventsDeleted)
|
|
268
|
+
eventOps.push(`-${counts.eventsDeleted}`);
|
|
269
|
+
parts.push(`events ${eventOps.join('/')}`);
|
|
270
|
+
}
|
|
271
|
+
if (counts.statesCreated || counts.statesUpdated || counts.statesDeleted) {
|
|
272
|
+
const stateOps = [];
|
|
273
|
+
if (counts.statesCreated)
|
|
274
|
+
stateOps.push(`+${counts.statesCreated}`);
|
|
275
|
+
if (counts.statesUpdated)
|
|
276
|
+
stateOps.push(`~${counts.statesUpdated}`);
|
|
277
|
+
if (counts.statesDeleted)
|
|
278
|
+
stateOps.push(`-${counts.statesDeleted}`);
|
|
279
|
+
parts.push(`states ${stateOps.join('/')}`);
|
|
280
|
+
}
|
|
281
|
+
return parts.join(', ');
|
|
282
|
+
}
|
|
283
|
+
//# sourceMappingURL=flow-metadata.js.map
|
package/dist/sync/push.js
CHANGED
|
@@ -1,8 +1,8 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* Push operations for changed files
|
|
3
3
|
*/
|
|
4
|
-
import { updateSkill, createAgent, createFlow, createSkill, publishFlow } from '../api.js';
|
|
5
|
-
import { ensureState, mapPath, skillMetadataPath, projectDir, agentMetadataPath } from '../fsutil.js';
|
|
4
|
+
import { updateSkill, createAgent, createFlow, createSkill, publishFlow, getFlow } from '../api.js';
|
|
5
|
+
import { ensureState, mapPath, skillMetadataPath, projectDir, agentMetadataPath, flowMetadataPath } from '../fsutil.js';
|
|
6
6
|
import { validateSkillFolder, getSingleSkillFile, getExtensionForRunner } from './skill-files.js';
|
|
7
7
|
import fs from 'fs-extra';
|
|
8
8
|
import { sha256, loadHashes, saveHashes } from '../hash.js';
|
|
@@ -11,6 +11,7 @@ import { generateFlowsYaml } from './metadata.js';
|
|
|
11
11
|
import { isProjectMap, isLegacyProjectMap } from './projects.js';
|
|
12
12
|
import { flowsYamlPath } from '../fsutil.js';
|
|
13
13
|
import { pushAllProjectAttributes } from './attributes.js';
|
|
14
|
+
import { syncFlowMetadata, emptyFlowSyncCounts, totalFlowSyncOps, describeFlowSyncCounts } from './flow-metadata.js';
|
|
14
15
|
/**
|
|
15
16
|
* Scan filesystem for local-only entities not in the project map yet
|
|
16
17
|
*/
|
|
@@ -444,6 +445,68 @@ export async function pushChanged(client, customer, verbose = false, shouldPubli
|
|
|
444
445
|
}
|
|
445
446
|
}
|
|
446
447
|
}
|
|
448
|
+
// Sync flow metadata (title, events, state_fields) for any flow whose
|
|
449
|
+
// metadata.yaml hash has changed. This closes the loop on GH issue #3:
|
|
450
|
+
// previously push only updated skill scripts, leaving local edits to
|
|
451
|
+
// flow events and title silently un-synced.
|
|
452
|
+
const flowSyncCounts = emptyFlowSyncCounts();
|
|
453
|
+
for (const [projectIdn, projectData] of Object.entries(projects)) {
|
|
454
|
+
for (const [agentIdn, agentObj] of Object.entries(projectData.agents)) {
|
|
455
|
+
for (const [flowIdn, flowObj] of Object.entries(agentObj.flows)) {
|
|
456
|
+
if (!flowObj.id)
|
|
457
|
+
continue;
|
|
458
|
+
const metaPath = flowMetadataPath(customer.idn, projectIdn, agentIdn, flowIdn);
|
|
459
|
+
if (!(await fs.pathExists(metaPath)))
|
|
460
|
+
continue;
|
|
461
|
+
const metaContent = await fs.readFile(metaPath, 'utf8');
|
|
462
|
+
const metaHash = sha256(metaContent);
|
|
463
|
+
const oldHash = hashes[metaPath];
|
|
464
|
+
if (oldHash === metaHash) {
|
|
465
|
+
if (verbose)
|
|
466
|
+
console.log(` ✓ Flow metadata unchanged: ${flowIdn}`);
|
|
467
|
+
continue;
|
|
468
|
+
}
|
|
469
|
+
if (verbose)
|
|
470
|
+
console.log(` 🔄 Flow metadata changed, syncing: ${agentIdn}/${flowIdn}`);
|
|
471
|
+
let localFlow;
|
|
472
|
+
try {
|
|
473
|
+
localFlow = yaml.load(metaContent);
|
|
474
|
+
}
|
|
475
|
+
catch (error) {
|
|
476
|
+
console.error(`❌ Failed to parse flow metadata for ${flowIdn}: ${error instanceof Error ? error.message : String(error)}`);
|
|
477
|
+
continue;
|
|
478
|
+
}
|
|
479
|
+
let remoteFlow = null;
|
|
480
|
+
try {
|
|
481
|
+
remoteFlow = await getFlow(client, flowObj.id);
|
|
482
|
+
}
|
|
483
|
+
catch (error) {
|
|
484
|
+
// 404 means the flow ID is stale; skip flow-level update but still
|
|
485
|
+
// try to sync children since list endpoints may still work.
|
|
486
|
+
if (verbose) {
|
|
487
|
+
console.log(` ⚠️ Could not GET flow ${flowIdn}: ${error.response?.status ?? error.message}`);
|
|
488
|
+
}
|
|
489
|
+
}
|
|
490
|
+
const opsBefore = totalFlowSyncOps(flowSyncCounts);
|
|
491
|
+
await syncFlowMetadata(client, flowObj.id, localFlow, remoteFlow, verbose, flowSyncCounts);
|
|
492
|
+
const opsAfter = totalFlowSyncOps(flowSyncCounts);
|
|
493
|
+
if (opsAfter > opsBefore) {
|
|
494
|
+
pushed += (opsAfter - opsBefore);
|
|
495
|
+
metadataChanged = true;
|
|
496
|
+
}
|
|
497
|
+
// Hash is updated regardless of whether ops happened, so we don't
|
|
498
|
+
// re-scan the same untouched flow on the next push.
|
|
499
|
+
newHashes[metaPath] = metaHash;
|
|
500
|
+
}
|
|
501
|
+
}
|
|
502
|
+
}
|
|
503
|
+
const totalFlowOps = totalFlowSyncOps(flowSyncCounts);
|
|
504
|
+
if (totalFlowOps > 0) {
|
|
505
|
+
console.log(`↑ Flow metadata synced: ${describeFlowSyncCounts(flowSyncCounts)}`);
|
|
506
|
+
}
|
|
507
|
+
if (flowSyncCounts.errors.length > 0) {
|
|
508
|
+
console.warn(`⚠️ ${flowSyncCounts.errors.length} flow-metadata error(s) during push.`);
|
|
509
|
+
}
|
|
447
510
|
if (verbose)
|
|
448
511
|
console.log(`🔄 Scanned ${scanned} files, found ${pushed} changes`);
|
|
449
512
|
// Push project attributes for all projects
|
package/dist/types.d.ts
CHANGED
|
@@ -441,6 +441,22 @@ export interface CreateFlowEventRequest {
|
|
|
441
441
|
export interface CreateFlowEventResponse {
|
|
442
442
|
id: string;
|
|
443
443
|
}
|
|
444
|
+
/**
|
|
445
|
+
* Payload for PATCH /api/v1/designer/flows/events/{eventId}
|
|
446
|
+
*
|
|
447
|
+
* Required fields per probe testing: idn, skill_selector, interrupt_mode.
|
|
448
|
+
* Sending the full event body is the platform's expected shape.
|
|
449
|
+
*/
|
|
450
|
+
export interface UpdateFlowEventRequest {
|
|
451
|
+
idn: string;
|
|
452
|
+
description?: string | null;
|
|
453
|
+
skill_selector: string;
|
|
454
|
+
skill_idn?: string | null;
|
|
455
|
+
state_idn?: string | null;
|
|
456
|
+
interrupt_mode: string;
|
|
457
|
+
integration_idn?: string | null;
|
|
458
|
+
connector_idn?: string | null;
|
|
459
|
+
}
|
|
444
460
|
export interface CreateFlowStateRequest {
|
|
445
461
|
title: string;
|
|
446
462
|
idn: string;
|
|
@@ -450,6 +466,27 @@ export interface CreateFlowStateRequest {
|
|
|
450
466
|
export interface CreateFlowStateResponse {
|
|
451
467
|
id: string;
|
|
452
468
|
}
|
|
469
|
+
/**
|
|
470
|
+
* Payload for PUT /api/v1/designer/flows/states/{stateId}
|
|
471
|
+
*/
|
|
472
|
+
export interface UpdateFlowStateRequest {
|
|
473
|
+
title: string;
|
|
474
|
+
idn: string;
|
|
475
|
+
default_value?: string;
|
|
476
|
+
scope: string;
|
|
477
|
+
}
|
|
478
|
+
/**
|
|
479
|
+
* Payload for PATCH /api/v1/designer/flows/{flowId}
|
|
480
|
+
*
|
|
481
|
+
* Empty body returns 500; the platform requires the full descriptor.
|
|
482
|
+
*/
|
|
483
|
+
export interface UpdateFlowRequest {
|
|
484
|
+
idn: string;
|
|
485
|
+
title: string;
|
|
486
|
+
description?: string;
|
|
487
|
+
default_runner_type: RunnerType;
|
|
488
|
+
default_model: ModelConfig;
|
|
489
|
+
}
|
|
453
490
|
export interface CreateSkillParameterRequest {
|
|
454
491
|
name: string;
|
|
455
492
|
default_value?: string;
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "newo",
|
|
3
|
-
"version": "3.7.
|
|
3
|
+
"version": "3.7.2",
|
|
4
4
|
"description": "NEWO CLI: Professional command-line tool with modular architecture for NEWO AI Agent development. Features account migration, integration management, webhook automation, AKB knowledge base, project attributes, sandbox testing, IDN-based file management, real-time progress tracking, intelligent sync operations, and comprehensive multi-customer support.",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"bin": {
|
package/src/api.ts
CHANGED
|
@@ -23,8 +23,11 @@ import type {
|
|
|
23
23
|
CreateSkillResponse,
|
|
24
24
|
CreateFlowEventRequest,
|
|
25
25
|
CreateFlowEventResponse,
|
|
26
|
+
UpdateFlowEventRequest,
|
|
26
27
|
CreateFlowStateRequest,
|
|
27
28
|
CreateFlowStateResponse,
|
|
29
|
+
UpdateFlowStateRequest,
|
|
30
|
+
UpdateFlowRequest,
|
|
28
31
|
CreateSkillParameterRequest,
|
|
29
32
|
CreateSkillParameterResponse,
|
|
30
33
|
CreateCustomerAttributeRequest,
|
|
@@ -151,6 +154,30 @@ export async function listFlowSkills(client: AxiosInstance, flowId: string): Pro
|
|
|
151
154
|
return response.data;
|
|
152
155
|
}
|
|
153
156
|
|
|
157
|
+
/**
|
|
158
|
+
* Fetch a single flow's top-level descriptor.
|
|
159
|
+
*
|
|
160
|
+
* Used by push to detect whether local metadata.yaml title/description
|
|
161
|
+
* differ from the platform before patching.
|
|
162
|
+
*/
|
|
163
|
+
export interface FlowDescriptor {
|
|
164
|
+
id: string;
|
|
165
|
+
idn: string;
|
|
166
|
+
title: string;
|
|
167
|
+
description: string | null;
|
|
168
|
+
agent_id: string;
|
|
169
|
+
default_runner_type: string;
|
|
170
|
+
default_model: { provider_idn: string; model_idn: string };
|
|
171
|
+
publication_datetime?: string;
|
|
172
|
+
last_change_datetime?: string;
|
|
173
|
+
creation_datetime?: string;
|
|
174
|
+
}
|
|
175
|
+
|
|
176
|
+
export async function getFlow(client: AxiosInstance, flowId: string): Promise<FlowDescriptor> {
|
|
177
|
+
const response = await client.get<FlowDescriptor>(`/api/v1/designer/flows/${flowId}`);
|
|
178
|
+
return response.data;
|
|
179
|
+
}
|
|
180
|
+
|
|
154
181
|
export async function getSkill(client: AxiosInstance, skillId: string): Promise<Skill> {
|
|
155
182
|
const response = await client.get<Skill>(`/api/v1/designer/skills/${skillId}`);
|
|
156
183
|
return response.data;
|
|
@@ -345,6 +372,14 @@ export async function createFlowEvent(client: AxiosInstance, flowId: string, eve
|
|
|
345
372
|
return response.data;
|
|
346
373
|
}
|
|
347
374
|
|
|
375
|
+
export async function updateFlowEvent(
|
|
376
|
+
client: AxiosInstance,
|
|
377
|
+
eventId: string,
|
|
378
|
+
eventData: UpdateFlowEventRequest
|
|
379
|
+
): Promise<void> {
|
|
380
|
+
await client.patch(`/api/v1/designer/flows/events/${eventId}`, eventData);
|
|
381
|
+
}
|
|
382
|
+
|
|
348
383
|
export async function deleteFlowEvent(client: AxiosInstance, eventId: string): Promise<void> {
|
|
349
384
|
await client.delete(`/api/v1/designer/flows/events/${eventId}`);
|
|
350
385
|
}
|
|
@@ -354,6 +389,32 @@ export async function createFlowState(client: AxiosInstance, flowId: string, sta
|
|
|
354
389
|
return response.data;
|
|
355
390
|
}
|
|
356
391
|
|
|
392
|
+
export async function updateFlowState(
|
|
393
|
+
client: AxiosInstance,
|
|
394
|
+
stateId: string,
|
|
395
|
+
stateData: UpdateFlowStateRequest
|
|
396
|
+
): Promise<void> {
|
|
397
|
+
await client.put(`/api/v1/designer/flows/states/${stateId}`, stateData);
|
|
398
|
+
}
|
|
399
|
+
|
|
400
|
+
export async function deleteFlowState(client: AxiosInstance, stateId: string): Promise<void> {
|
|
401
|
+
await client.delete(`/api/v1/designer/flows/states/${stateId}`);
|
|
402
|
+
}
|
|
403
|
+
|
|
404
|
+
/**
|
|
405
|
+
* Update flow metadata (title, description, runner type, default model).
|
|
406
|
+
*
|
|
407
|
+
* Uses PATCH /api/v1/designer/flows/{flowId}. The platform requires the
|
|
408
|
+
* full flow descriptor; sending a partial body returns 500.
|
|
409
|
+
*/
|
|
410
|
+
export async function updateFlow(
|
|
411
|
+
client: AxiosInstance,
|
|
412
|
+
flowId: string,
|
|
413
|
+
flowData: UpdateFlowRequest
|
|
414
|
+
): Promise<void> {
|
|
415
|
+
await client.patch(`/api/v1/designer/flows/${flowId}`, flowData);
|
|
416
|
+
}
|
|
417
|
+
|
|
357
418
|
export async function createSkillParameter(client: AxiosInstance, skillId: string, paramData: CreateSkillParameterRequest): Promise<CreateSkillParameterResponse> {
|
|
358
419
|
// Debug the parameter creation request
|
|
359
420
|
console.log('Creating parameter for skill:', skillId);
|
|
@@ -49,7 +49,14 @@ import {
|
|
|
49
49
|
publishFlow,
|
|
50
50
|
listLibraries,
|
|
51
51
|
updateLibrarySkill,
|
|
52
|
+
getFlow,
|
|
52
53
|
} from '../../../api.js';
|
|
54
|
+
import {
|
|
55
|
+
syncFlowMetadata,
|
|
56
|
+
emptyFlowSyncCounts,
|
|
57
|
+
totalFlowSyncOps,
|
|
58
|
+
describeFlowSyncCounts
|
|
59
|
+
} from '../../../sync/flow-metadata.js';
|
|
53
60
|
import {
|
|
54
61
|
ensureState,
|
|
55
62
|
writeFileSafe,
|
|
@@ -540,6 +547,24 @@ export class ProjectSyncStrategy implements ISyncStrategy<ProjectMeta, LocalProj
|
|
|
540
547
|
for (const change of changes) {
|
|
541
548
|
try {
|
|
542
549
|
if (change.operation === 'modified') {
|
|
550
|
+
// Flow-level metadata.yaml needs different handling than a skill
|
|
551
|
+
// script: we sync title/events/state_fields rather than uploading a
|
|
552
|
+
// file. Detected by filename. (GH issue #3)
|
|
553
|
+
if (change.path.endsWith('/metadata.yaml') && !change.path.includes('/libraries/')) {
|
|
554
|
+
const pathParts = change.path.split('/');
|
|
555
|
+
// {customer}/projects/{project}/{agent}/{flow}/metadata.yaml
|
|
556
|
+
// Last 5 segments end with metadata.yaml; skip if it's a skill
|
|
557
|
+
// metadata file (one extra segment) - skill metadata is handled
|
|
558
|
+
// by V1 legacy push, not by this strategy yet.
|
|
559
|
+
const tail = pathParts.slice(-5);
|
|
560
|
+
const isFlowMeta = tail[0] === 'projects' || tail[2] && tail[4] === 'metadata.yaml';
|
|
561
|
+
if (isFlowMeta && tail.length === 5) {
|
|
562
|
+
const updateResult = await this.pushFlowMetadataUpdate(client, change, mapData, newHashes);
|
|
563
|
+
result.updated += updateResult;
|
|
564
|
+
continue;
|
|
565
|
+
}
|
|
566
|
+
}
|
|
567
|
+
|
|
543
568
|
const isLibrary = change.path.includes('/libraries/');
|
|
544
569
|
if (isLibrary) {
|
|
545
570
|
const updateResult = await this.pushLibrarySkillUpdate(client, change, mapData, newHashes);
|
|
@@ -568,6 +593,64 @@ export class ProjectSyncStrategy implements ISyncStrategy<ProjectMeta, LocalProj
|
|
|
568
593
|
return result;
|
|
569
594
|
}
|
|
570
595
|
|
|
596
|
+
/**
|
|
597
|
+
* Push a flow metadata.yaml change — syncs title, events, and state_fields
|
|
598
|
+
* to the platform. Closes GH issue #3 (events/title silently un-synced).
|
|
599
|
+
*
|
|
600
|
+
* Path shape: newo_customers/{customer}/projects/{project}/{agent}/{flow}/metadata.yaml
|
|
601
|
+
*/
|
|
602
|
+
private async pushFlowMetadataUpdate(
|
|
603
|
+
client: AxiosInstance,
|
|
604
|
+
change: ChangeItem<LocalProjectData>,
|
|
605
|
+
mapData: ProjectMap,
|
|
606
|
+
newHashes: HashStore
|
|
607
|
+
): Promise<number> {
|
|
608
|
+
const pathParts = change.path.split('/');
|
|
609
|
+
// Tail: projects/{project}/{agent}/{flow}/metadata.yaml
|
|
610
|
+
const flowIdn = pathParts[pathParts.length - 2] || '';
|
|
611
|
+
const agentIdn = pathParts[pathParts.length - 3] || '';
|
|
612
|
+
const projectIdn = pathParts[pathParts.length - 4] || '';
|
|
613
|
+
|
|
614
|
+
const projectData = mapData.projects[projectIdn];
|
|
615
|
+
const agentData = projectData?.agents[agentIdn];
|
|
616
|
+
const flowData = agentData?.flows[flowIdn];
|
|
617
|
+
|
|
618
|
+
if (!flowData?.id) {
|
|
619
|
+
this.logger.warn(`Flow metadata change but flow not in project map: ${projectIdn}/${agentIdn}/${flowIdn}`);
|
|
620
|
+
return 0;
|
|
621
|
+
}
|
|
622
|
+
|
|
623
|
+
const content = await fs.readFile(change.path, 'utf8');
|
|
624
|
+
let localFlow: FlowMetadata;
|
|
625
|
+
try {
|
|
626
|
+
localFlow = yaml.load(content) as FlowMetadata;
|
|
627
|
+
} catch (error) {
|
|
628
|
+
throw new Error(`Failed to parse ${change.path}: ${error instanceof Error ? error.message : String(error)}`);
|
|
629
|
+
}
|
|
630
|
+
|
|
631
|
+
let remoteFlow = null;
|
|
632
|
+
try {
|
|
633
|
+
remoteFlow = await getFlow(client, flowData.id);
|
|
634
|
+
} catch (error: any) {
|
|
635
|
+
this.logger.verbose(`Could not GET flow ${flowIdn}: ${error.response?.status ?? error.message}`);
|
|
636
|
+
}
|
|
637
|
+
|
|
638
|
+
const counts = emptyFlowSyncCounts();
|
|
639
|
+
await syncFlowMetadata(client, flowData.id, localFlow, remoteFlow, false, counts);
|
|
640
|
+
|
|
641
|
+
const total = totalFlowSyncOps(counts);
|
|
642
|
+
if (total > 0) {
|
|
643
|
+
this.logger.info(`↑ Flow ${flowIdn}: ${describeFlowSyncCounts(counts)}`);
|
|
644
|
+
}
|
|
645
|
+
for (const err of counts.errors) {
|
|
646
|
+
this.logger.warn(err);
|
|
647
|
+
}
|
|
648
|
+
|
|
649
|
+
// Stamp the hash so the next push skips this file unless it changes again.
|
|
650
|
+
newHashes[change.path] = sha256(content);
|
|
651
|
+
return total;
|
|
652
|
+
}
|
|
653
|
+
|
|
571
654
|
/**
|
|
572
655
|
* Push a skill update
|
|
573
656
|
*/
|
|
@@ -709,6 +792,23 @@ export class ProjectSyncStrategy implements ISyncStrategy<ProjectMeta, LocalProj
|
|
|
709
792
|
for (const [projectIdn, projectData] of Object.entries(mapData.projects)) {
|
|
710
793
|
for (const [agentIdn, agentData] of Object.entries(projectData.agents)) {
|
|
711
794
|
for (const [flowIdn, flowData] of Object.entries(agentData.flows)) {
|
|
795
|
+
// Flow metadata change detection (title/events/state_fields).
|
|
796
|
+
// Surfaced as a change so push() picks it up alongside skill edits.
|
|
797
|
+
const flowMetaPath = flowMetadataPath(customer.idn, projectIdn, agentIdn, flowIdn);
|
|
798
|
+
if (await fs.pathExists(flowMetaPath)) {
|
|
799
|
+
const content = await fs.readFile(flowMetaPath, 'utf8');
|
|
800
|
+
const currentHash = sha256(content);
|
|
801
|
+
const storedHash = hashes[flowMetaPath];
|
|
802
|
+
|
|
803
|
+
if (storedHash !== currentHash) {
|
|
804
|
+
changes.push({
|
|
805
|
+
item: {} as LocalProjectData,
|
|
806
|
+
operation: 'modified',
|
|
807
|
+
path: flowMetaPath
|
|
808
|
+
});
|
|
809
|
+
}
|
|
810
|
+
}
|
|
811
|
+
|
|
712
812
|
for (const [skillIdn, _skillData] of Object.entries(flowData.skills)) {
|
|
713
813
|
const skillFile = await getSingleSkillFile(customer.idn, projectIdn, agentIdn, flowIdn, skillIdn);
|
|
714
814
|
|