newo 3.7.0 → 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 +20 -1
- package/dist/api.d.ts +33 -1
- package/dist/api.js +22 -0
- package/dist/domain/strategies/sync/AttributeSyncStrategy.js +38 -8
- 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/attributes.js +38 -12
- package/dist/sync/flow-metadata.d.ts +67 -0
- package/dist/sync/flow-metadata.js +283 -0
- package/dist/sync/json-attr-utils.d.ts +67 -0
- package/dist/sync/json-attr-utils.js +98 -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/AttributeSyncStrategy.ts +45 -8
- package/src/domain/strategies/sync/ProjectSyncStrategy.ts +100 -0
- package/src/domain/strategies/sync/V2ProjectSyncStrategy.ts +147 -0
- package/src/sync/attributes.ts +43 -14
- package/src/sync/flow-metadata.ts +345 -0
- package/src/sync/json-attr-utils.ts +95 -0
- package/src/sync/push.ts +75 -2
- package/src/types.ts +40 -0
package/CHANGELOG.md
CHANGED
|
@@ -7,6 +7,24 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
|
|
|
7
7
|
|
|
8
8
|
## [Unreleased]
|
|
9
9
|
|
|
10
|
+
## [3.7.2] - 2026-05-17
|
|
11
|
+
|
|
12
|
+
### Fixed
|
|
13
|
+
|
|
14
|
+
- **`newo push` now syncs flow metadata (title, events, state_fields) to the platform** — closes [#3](https://github.com/sabbah13/newo-cli/issues/3). Previously `newo push` only uploaded skill scripts; edits to a flow's `metadata.yaml` (V1) or `{FlowIdn}.yaml` (V2) silently never reached the platform, and events added via `newo create-event` could appear to disappear after a subsequent push because the local-side definition lagged the remote one. Push now performs hash-gated reconciliation per flow: when `metadata.yaml` changes locally it calls `PATCH /api/v1/designer/flows/{id}` for title/description/runner, then create/update/delete child events and state_fields against `/events` and `/states` so the platform mirrors local. Hash-gating is critical — flows whose `metadata.yaml` is untouched are never even compared, so stale local trees cannot accidentally wipe events created out-of-band via the Builder UI. Wired into the legacy `pushChanged` path, `ProjectSyncStrategy`, and `V2ProjectSyncStrategy`. Reported by Bob (issue #3).
|
|
15
|
+
|
|
16
|
+
### Added
|
|
17
|
+
|
|
18
|
+
- **`updateFlow`, `updateFlowEvent`, `updateFlowState`, `deleteFlowState`, `getFlow` API wrappers** in `src/api.ts` for the previously-undocumented `PATCH /api/v1/designer/flows/{id}`, `PATCH /api/v1/designer/flows/events/{id}`, `PUT /api/v1/designer/flows/states/{id}`, `DELETE /api/v1/designer/flows/states/{id}`, and `GET /api/v1/designer/flows/{id}` endpoints. Empty payload returns 500; the platform requires the full descriptor.
|
|
19
|
+
- **`src/sync/flow-metadata.ts`** module — pure-logic reconciler shared by both V1 and V2 push paths. Comparator helpers `flowEventDiffers`/`flowStateDiffers` normalize null/undefined/empty so YAML round-trips don't trigger spurious updates.
|
|
20
|
+
- **18 unit tests** in `test/flow-metadata.test.js` covering create/update/delete event flows, state CRUD in a single pass, no-op when local matches remote, and graceful error handling that records failures without aborting the loop.
|
|
21
|
+
|
|
22
|
+
## [3.7.1] - 2026-04-29
|
|
23
|
+
|
|
24
|
+
### Fixed
|
|
25
|
+
|
|
26
|
+
- **Workflow Builder canvas blank-screen after `newo push --only attributes`** - JSON-typed project/customer attributes (e.g. `project_attributes_private_dynamic_workflow_builder_canvas`) are now always coerced to a JSON STRING when persisted to `attributes.yaml` and when sent on push. Previously, when the API returned the `value` field as a parsed object, `yaml.dump` serialized it as a YAML structure and the next push sent `{"value": {...object...}}` instead of `{"value": "...json..."}` - the platform stored a shape Builder could not render and the canvas blanked out. Change-detection now compares both sides as canonical (compact) JSON, so pretty- vs compact-printed forms and string vs object representations no longer trigger spurious pushes. String-typed values are left bit-for-bit untouched, so no churn on existing repos. New helpers in `src/sync/json-attr-utils.ts` are wired into both `src/sync/attributes.ts` and `src/domain/strategies/sync/AttributeSyncStrategy.ts`. Reported by Bob; 19 regression tests in `test/json-attribute-roundtrip.test.js`.
|
|
27
|
+
|
|
10
28
|
## [3.7.0] - 2026-04-23
|
|
11
29
|
|
|
12
30
|
### Added
|
|
@@ -1033,7 +1051,8 @@ Another Item: $Price [Modifiers: modifier3]
|
|
|
1033
1051
|
- GitHub Actions CI/CD integration
|
|
1034
1052
|
- Robust authentication with token refresh
|
|
1035
1053
|
|
|
1036
|
-
[Unreleased]: https://github.com/sabbah13/newo-cli/compare/v3.
|
|
1054
|
+
[Unreleased]: https://github.com/sabbah13/newo-cli/compare/v3.7.1...HEAD
|
|
1055
|
+
[3.7.1]: https://github.com/sabbah13/newo-cli/compare/v3.7.0...v3.7.1
|
|
1037
1056
|
[3.3.0]: https://github.com/sabbah13/newo-cli/compare/v3.2.0...v3.3.0
|
|
1038
1057
|
[3.2.0]: https://github.com/sabbah13/newo-cli/compare/v3.1.0...v3.2.0
|
|
1039
1058
|
[3.1.0]: https://github.com/sabbah13/newo-cli/compare/v3.0.0...v3.1.0
|
package/dist/api.d.ts
CHANGED
|
@@ -1,10 +1,32 @@
|
|
|
1
1
|
import { type AxiosInstance } from 'axios';
|
|
2
|
-
import type { ProjectMeta, Agent, Skill, FlowEvent, FlowState, AkbImportArticle, CustomerProfile, CustomerAttribute, CustomerAttributesResponse, UserPersonaResponse, UserPersona, ChatHistoryParams, ChatHistoryResponse, CreateAgentRequest, CreateAgentResponse, CreateFlowRequest, CreateFlowResponse, CreateSkillRequest, CreateSkillResponse, CreateFlowEventRequest, CreateFlowEventResponse, CreateFlowStateRequest, CreateFlowStateResponse, CreateSkillParameterRequest, CreateSkillParameterResponse, CreateCustomerAttributeRequest, CreateCustomerAttributeResponse, CreatePersonaRequest, CreatePersonaResponse, CreateProjectRequest, CreateProjectResponse, PublishFlowRequest, PublishFlowResponse, Integration, Connector, CreateSandboxPersonaRequest, CreateSandboxPersonaResponse, CreateActorRequest, CreateActorResponse, SendChatMessageRequest, ConversationActsParams, ConversationActsResponse, ScriptAction, IntegrationSetting, CreateConnectorRequest, CreateConnectorResponse, UpdateConnectorRequest, OutgoingWebhook, IncomingWebhook, PersonaSearchResponse, AkbTopicsResponse, Registry, RegistryItem, AddProjectFromRegistryRequest, CreateNewoCustomerRequest, CreateNewoCustomerResponse, LogsQueryParams, LogsResponse } from './types.js';
|
|
2
|
+
import type { ProjectMeta, Agent, Skill, FlowEvent, FlowState, AkbImportArticle, CustomerProfile, CustomerAttribute, CustomerAttributesResponse, UserPersonaResponse, UserPersona, ChatHistoryParams, ChatHistoryResponse, CreateAgentRequest, CreateAgentResponse, CreateFlowRequest, CreateFlowResponse, CreateSkillRequest, CreateSkillResponse, CreateFlowEventRequest, CreateFlowEventResponse, UpdateFlowEventRequest, CreateFlowStateRequest, CreateFlowStateResponse, UpdateFlowStateRequest, UpdateFlowRequest, CreateSkillParameterRequest, CreateSkillParameterResponse, CreateCustomerAttributeRequest, CreateCustomerAttributeResponse, CreatePersonaRequest, CreatePersonaResponse, CreateProjectRequest, CreateProjectResponse, PublishFlowRequest, PublishFlowResponse, Integration, Connector, CreateSandboxPersonaRequest, CreateSandboxPersonaResponse, CreateActorRequest, CreateActorResponse, SendChatMessageRequest, ConversationActsParams, ConversationActsResponse, ScriptAction, IntegrationSetting, CreateConnectorRequest, CreateConnectorResponse, UpdateConnectorRequest, OutgoingWebhook, IncomingWebhook, PersonaSearchResponse, AkbTopicsResponse, Registry, RegistryItem, AddProjectFromRegistryRequest, CreateNewoCustomerRequest, CreateNewoCustomerResponse, LogsQueryParams, LogsResponse } from './types.js';
|
|
3
3
|
export declare function makeClient(verbose?: boolean, token?: string): Promise<AxiosInstance>;
|
|
4
4
|
export declare function listProjects(client: AxiosInstance): Promise<ProjectMeta[]>;
|
|
5
5
|
export declare function listAgents(client: AxiosInstance, projectId: string): Promise<Agent[]>;
|
|
6
6
|
export declare function getProjectMeta(client: AxiosInstance, projectId: string): Promise<ProjectMeta>;
|
|
7
7
|
export declare function listFlowSkills(client: AxiosInstance, flowId: string): Promise<Skill[]>;
|
|
8
|
+
/**
|
|
9
|
+
* Fetch a single flow's top-level descriptor.
|
|
10
|
+
*
|
|
11
|
+
* Used by push to detect whether local metadata.yaml title/description
|
|
12
|
+
* differ from the platform before patching.
|
|
13
|
+
*/
|
|
14
|
+
export interface FlowDescriptor {
|
|
15
|
+
id: string;
|
|
16
|
+
idn: string;
|
|
17
|
+
title: string;
|
|
18
|
+
description: string | null;
|
|
19
|
+
agent_id: string;
|
|
20
|
+
default_runner_type: string;
|
|
21
|
+
default_model: {
|
|
22
|
+
provider_idn: string;
|
|
23
|
+
model_idn: string;
|
|
24
|
+
};
|
|
25
|
+
publication_datetime?: string;
|
|
26
|
+
last_change_datetime?: string;
|
|
27
|
+
creation_datetime?: string;
|
|
28
|
+
}
|
|
29
|
+
export declare function getFlow(client: AxiosInstance, flowId: string): Promise<FlowDescriptor>;
|
|
8
30
|
export declare function getSkill(client: AxiosInstance, skillId: string): Promise<Skill>;
|
|
9
31
|
export declare function updateSkill(client: AxiosInstance, skillObject: Skill): Promise<void>;
|
|
10
32
|
export declare function listFlowEvents(client: AxiosInstance, flowId: string): Promise<FlowEvent[]>;
|
|
@@ -32,8 +54,18 @@ export declare function createSkill(client: AxiosInstance, flowId: string, skill
|
|
|
32
54
|
export declare function deleteSkill(client: AxiosInstance, skillId: string): Promise<void>;
|
|
33
55
|
export declare function deleteSkillById(client: AxiosInstance, skillId: string): Promise<void>;
|
|
34
56
|
export declare function createFlowEvent(client: AxiosInstance, flowId: string, eventData: CreateFlowEventRequest): Promise<CreateFlowEventResponse>;
|
|
57
|
+
export declare function updateFlowEvent(client: AxiosInstance, eventId: string, eventData: UpdateFlowEventRequest): Promise<void>;
|
|
35
58
|
export declare function deleteFlowEvent(client: AxiosInstance, eventId: string): Promise<void>;
|
|
36
59
|
export declare function createFlowState(client: AxiosInstance, flowId: string, stateData: CreateFlowStateRequest): Promise<CreateFlowStateResponse>;
|
|
60
|
+
export declare function updateFlowState(client: AxiosInstance, stateId: string, stateData: UpdateFlowStateRequest): Promise<void>;
|
|
61
|
+
export declare function deleteFlowState(client: AxiosInstance, stateId: string): Promise<void>;
|
|
62
|
+
/**
|
|
63
|
+
* Update flow metadata (title, description, runner type, default model).
|
|
64
|
+
*
|
|
65
|
+
* Uses PATCH /api/v1/designer/flows/{flowId}. The platform requires the
|
|
66
|
+
* full flow descriptor; sending a partial body returns 500.
|
|
67
|
+
*/
|
|
68
|
+
export declare function updateFlow(client: AxiosInstance, flowId: string, flowData: UpdateFlowRequest): Promise<void>;
|
|
37
69
|
export declare function createSkillParameter(client: AxiosInstance, skillId: string, paramData: CreateSkillParameterRequest): Promise<CreateSkillParameterResponse>;
|
|
38
70
|
export declare function createCustomerAttribute(client: AxiosInstance, attributeData: CreateCustomerAttributeRequest): Promise<CreateCustomerAttributeResponse>;
|
|
39
71
|
export declare function createProject(client: AxiosInstance, projectData: CreateProjectRequest): Promise<CreateProjectResponse>;
|
package/dist/api.js
CHANGED
|
@@ -77,6 +77,10 @@ export async function listFlowSkills(client, flowId) {
|
|
|
77
77
|
const response = await client.get(`/api/v1/designer/flows/${flowId}/skills`);
|
|
78
78
|
return response.data;
|
|
79
79
|
}
|
|
80
|
+
export async function getFlow(client, flowId) {
|
|
81
|
+
const response = await client.get(`/api/v1/designer/flows/${flowId}`);
|
|
82
|
+
return response.data;
|
|
83
|
+
}
|
|
80
84
|
export async function getSkill(client, skillId) {
|
|
81
85
|
const response = await client.get(`/api/v1/designer/skills/${skillId}`);
|
|
82
86
|
return response.data;
|
|
@@ -221,6 +225,9 @@ export async function createFlowEvent(client, flowId, eventData) {
|
|
|
221
225
|
const response = await client.post(`/api/v1/designer/flows/${flowId}/events`, eventData);
|
|
222
226
|
return response.data;
|
|
223
227
|
}
|
|
228
|
+
export async function updateFlowEvent(client, eventId, eventData) {
|
|
229
|
+
await client.patch(`/api/v1/designer/flows/events/${eventId}`, eventData);
|
|
230
|
+
}
|
|
224
231
|
export async function deleteFlowEvent(client, eventId) {
|
|
225
232
|
await client.delete(`/api/v1/designer/flows/events/${eventId}`);
|
|
226
233
|
}
|
|
@@ -228,6 +235,21 @@ export async function createFlowState(client, flowId, stateData) {
|
|
|
228
235
|
const response = await client.post(`/api/v1/designer/flows/${flowId}/states`, stateData);
|
|
229
236
|
return response.data;
|
|
230
237
|
}
|
|
238
|
+
export async function updateFlowState(client, stateId, stateData) {
|
|
239
|
+
await client.put(`/api/v1/designer/flows/states/${stateId}`, stateData);
|
|
240
|
+
}
|
|
241
|
+
export async function deleteFlowState(client, stateId) {
|
|
242
|
+
await client.delete(`/api/v1/designer/flows/states/${stateId}`);
|
|
243
|
+
}
|
|
244
|
+
/**
|
|
245
|
+
* Update flow metadata (title, description, runner type, default model).
|
|
246
|
+
*
|
|
247
|
+
* Uses PATCH /api/v1/designer/flows/{flowId}. The platform requires the
|
|
248
|
+
* full flow descriptor; sending a partial body returns 500.
|
|
249
|
+
*/
|
|
250
|
+
export async function updateFlow(client, flowId, flowData) {
|
|
251
|
+
await client.patch(`/api/v1/designer/flows/${flowId}`, flowData);
|
|
252
|
+
}
|
|
231
253
|
export async function createSkillParameter(client, skillId, paramData) {
|
|
232
254
|
// Debug the parameter creation request
|
|
233
255
|
console.log('Creating parameter for skill:', skillId);
|
|
@@ -15,6 +15,7 @@ import path from 'path';
|
|
|
15
15
|
import { getCustomerAttributes, getProjectAttributes, updateCustomerAttribute, updateProjectAttribute, listProjects } from '../../../api.js';
|
|
16
16
|
import { writeFileSafe, customerAttributesPath, customerAttributesMapPath } from '../../../fsutil.js';
|
|
17
17
|
import { patchYamlToPyyaml } from '../../../format/yaml-patch.js';
|
|
18
|
+
import { isJsonValueType, normalizeJsonValueForStorage, jsonValuesEqual } from '../../../sync/json-attr-utils.js';
|
|
18
19
|
import { sha256, saveHashes, loadHashes } from '../../../hash.js';
|
|
19
20
|
/**
|
|
20
21
|
* AttributeSyncStrategy - Handles attribute synchronization
|
|
@@ -142,8 +143,15 @@ export class AttributeSyncStrategy {
|
|
|
142
143
|
*/
|
|
143
144
|
cleanAttribute(attr) {
|
|
144
145
|
let processedValue = attr.value;
|
|
145
|
-
//
|
|
146
|
-
|
|
146
|
+
// Coerce JSON-typed values to a STRING. The API may return parsed
|
|
147
|
+
// objects for `value_type: json`; if we let yaml.dump turn them into
|
|
148
|
+
// YAML structures, the next push sends an object and the Workflow
|
|
149
|
+
// Builder canvas blanks out. See src/sync/json-attr-utils.ts.
|
|
150
|
+
if (isJsonValueType(attr.value_type)) {
|
|
151
|
+
processedValue = normalizeJsonValueForStorage(attr.value);
|
|
152
|
+
}
|
|
153
|
+
else if (typeof attr.value === 'string' && attr.value.startsWith('[{') && attr.value.endsWith('}]')) {
|
|
154
|
+
// Legacy: reformat array-of-objects JSON strings for readability
|
|
147
155
|
try {
|
|
148
156
|
const parsed = JSON.parse(attr.value);
|
|
149
157
|
processedValue = JSON.stringify(parsed, null, 0);
|
|
@@ -243,10 +251,22 @@ export class AttributeSyncStrategy {
|
|
|
243
251
|
const remoteAttr = remoteMap.get(localAttr.idn);
|
|
244
252
|
if (!remoteAttr)
|
|
245
253
|
continue;
|
|
246
|
-
|
|
254
|
+
// For JSON-typed attrs, compare canonical JSON (handles
|
|
255
|
+
// pretty/compact and string/object differences). Always send the
|
|
256
|
+
// value as a STRING so the platform stores the canvas the way the
|
|
257
|
+
// Workflow Builder expects to read it back.
|
|
258
|
+
const isJson = isJsonValueType(localAttr.value_type);
|
|
259
|
+
const valuesAreEqual = isJson
|
|
260
|
+
? jsonValuesEqual(localAttr.value, remoteAttr.value)
|
|
261
|
+
: String(localAttr.value) === String(remoteAttr.value);
|
|
262
|
+
if (!valuesAreEqual) {
|
|
263
|
+
const valueToSend = isJson
|
|
264
|
+
? normalizeJsonValueForStorage(localAttr.value)
|
|
265
|
+
: localAttr.value;
|
|
247
266
|
await updateCustomerAttribute(client, {
|
|
248
|
-
|
|
249
|
-
|
|
267
|
+
...localAttr,
|
|
268
|
+
value: valueToSend,
|
|
269
|
+
id: attributeId
|
|
250
270
|
});
|
|
251
271
|
updatedCount++;
|
|
252
272
|
this.logger.info(` ✓ Updated customer attribute: ${localAttr.idn}`);
|
|
@@ -286,10 +306,20 @@ export class AttributeSyncStrategy {
|
|
|
286
306
|
const remoteAttr = remoteMap.get(localAttr.idn);
|
|
287
307
|
if (!remoteAttr)
|
|
288
308
|
continue;
|
|
289
|
-
|
|
309
|
+
// Same canonical-JSON / always-string-on-push policy as customer
|
|
310
|
+
// attributes (see pushCustomerAttributes for rationale).
|
|
311
|
+
const isJson = isJsonValueType(localAttr.value_type);
|
|
312
|
+
const valuesAreEqual = isJson
|
|
313
|
+
? jsonValuesEqual(localAttr.value, remoteAttr.value)
|
|
314
|
+
: String(localAttr.value) === String(remoteAttr.value);
|
|
315
|
+
if (!valuesAreEqual) {
|
|
316
|
+
const valueToSend = isJson
|
|
317
|
+
? normalizeJsonValueForStorage(localAttr.value)
|
|
318
|
+
: localAttr.value;
|
|
290
319
|
await updateProjectAttribute(client, project.id, {
|
|
291
|
-
|
|
292
|
-
|
|
320
|
+
...localAttr,
|
|
321
|
+
value: valueToSend,
|
|
322
|
+
id: attributeId
|
|
293
323
|
});
|
|
294
324
|
updatedCount++;
|
|
295
325
|
this.logger.info(` ✓ Updated project attribute: ${projectIdn}/${localAttr.idn}`);
|
|
@@ -75,6 +75,13 @@ export declare class ProjectSyncStrategy implements ISyncStrategy<ProjectMeta, L
|
|
|
75
75
|
* Push changed projects to NEWO platform
|
|
76
76
|
*/
|
|
77
77
|
push(customer: CustomerConfig, changes?: ChangeItem<LocalProjectData>[]): Promise<PushResult>;
|
|
78
|
+
/**
|
|
79
|
+
* Push a flow metadata.yaml change — syncs title, events, and state_fields
|
|
80
|
+
* to the platform. Closes GH issue #3 (events/title silently un-synced).
|
|
81
|
+
*
|
|
82
|
+
* Path shape: newo_customers/{customer}/projects/{project}/{agent}/{flow}/metadata.yaml
|
|
83
|
+
*/
|
|
84
|
+
private pushFlowMetadataUpdate;
|
|
78
85
|
/**
|
|
79
86
|
* Push a skill update
|
|
80
87
|
*/
|
|
@@ -12,7 +12,8 @@
|
|
|
12
12
|
*/
|
|
13
13
|
import fs from 'fs-extra';
|
|
14
14
|
import yaml from 'js-yaml';
|
|
15
|
-
import { listProjects, listAgents, listFlowSkills, listFlowEvents, listFlowStates, updateSkill, publishFlow, listLibraries, updateLibrarySkill, } from '../../../api.js';
|
|
15
|
+
import { listProjects, listAgents, listFlowSkills, listFlowEvents, listFlowStates, updateSkill, publishFlow, listLibraries, updateLibrarySkill, getFlow, } from '../../../api.js';
|
|
16
|
+
import { syncFlowMetadata, emptyFlowSyncCounts, totalFlowSyncOps, describeFlowSyncCounts } from '../../../sync/flow-metadata.js';
|
|
16
17
|
import { ensureState, writeFileSafe, mapPath, projectMetadataPath, agentMetadataPath, flowMetadataPath, skillMetadataPath, skillScriptPath, skillFolderPath, flowsYamlPath, customerProjectsDir, projectDir, libraryMetadataPath, librarySkillMetadataPath, librarySkillScriptPath, } from '../../../fsutil.js';
|
|
17
18
|
import { sha256, saveHashes, loadHashes } from '../../../hash.js';
|
|
18
19
|
import { generateFlowsYaml } from '../../../sync/metadata.js';
|
|
@@ -354,6 +355,23 @@ export class ProjectSyncStrategy {
|
|
|
354
355
|
for (const change of changes) {
|
|
355
356
|
try {
|
|
356
357
|
if (change.operation === 'modified') {
|
|
358
|
+
// Flow-level metadata.yaml needs different handling than a skill
|
|
359
|
+
// script: we sync title/events/state_fields rather than uploading a
|
|
360
|
+
// file. Detected by filename. (GH issue #3)
|
|
361
|
+
if (change.path.endsWith('/metadata.yaml') && !change.path.includes('/libraries/')) {
|
|
362
|
+
const pathParts = change.path.split('/');
|
|
363
|
+
// {customer}/projects/{project}/{agent}/{flow}/metadata.yaml
|
|
364
|
+
// Last 5 segments end with metadata.yaml; skip if it's a skill
|
|
365
|
+
// metadata file (one extra segment) - skill metadata is handled
|
|
366
|
+
// by V1 legacy push, not by this strategy yet.
|
|
367
|
+
const tail = pathParts.slice(-5);
|
|
368
|
+
const isFlowMeta = tail[0] === 'projects' || tail[2] && tail[4] === 'metadata.yaml';
|
|
369
|
+
if (isFlowMeta && tail.length === 5) {
|
|
370
|
+
const updateResult = await this.pushFlowMetadataUpdate(client, change, mapData, newHashes);
|
|
371
|
+
result.updated += updateResult;
|
|
372
|
+
continue;
|
|
373
|
+
}
|
|
374
|
+
}
|
|
357
375
|
const isLibrary = change.path.includes('/libraries/');
|
|
358
376
|
if (isLibrary) {
|
|
359
377
|
const updateResult = await this.pushLibrarySkillUpdate(client, change, mapData, newHashes);
|
|
@@ -381,6 +399,53 @@ export class ProjectSyncStrategy {
|
|
|
381
399
|
}
|
|
382
400
|
return result;
|
|
383
401
|
}
|
|
402
|
+
/**
|
|
403
|
+
* Push a flow metadata.yaml change — syncs title, events, and state_fields
|
|
404
|
+
* to the platform. Closes GH issue #3 (events/title silently un-synced).
|
|
405
|
+
*
|
|
406
|
+
* Path shape: newo_customers/{customer}/projects/{project}/{agent}/{flow}/metadata.yaml
|
|
407
|
+
*/
|
|
408
|
+
async pushFlowMetadataUpdate(client, change, mapData, newHashes) {
|
|
409
|
+
const pathParts = change.path.split('/');
|
|
410
|
+
// Tail: projects/{project}/{agent}/{flow}/metadata.yaml
|
|
411
|
+
const flowIdn = pathParts[pathParts.length - 2] || '';
|
|
412
|
+
const agentIdn = pathParts[pathParts.length - 3] || '';
|
|
413
|
+
const projectIdn = pathParts[pathParts.length - 4] || '';
|
|
414
|
+
const projectData = mapData.projects[projectIdn];
|
|
415
|
+
const agentData = projectData?.agents[agentIdn];
|
|
416
|
+
const flowData = agentData?.flows[flowIdn];
|
|
417
|
+
if (!flowData?.id) {
|
|
418
|
+
this.logger.warn(`Flow metadata change but flow not in project map: ${projectIdn}/${agentIdn}/${flowIdn}`);
|
|
419
|
+
return 0;
|
|
420
|
+
}
|
|
421
|
+
const content = await fs.readFile(change.path, 'utf8');
|
|
422
|
+
let localFlow;
|
|
423
|
+
try {
|
|
424
|
+
localFlow = yaml.load(content);
|
|
425
|
+
}
|
|
426
|
+
catch (error) {
|
|
427
|
+
throw new Error(`Failed to parse ${change.path}: ${error instanceof Error ? error.message : String(error)}`);
|
|
428
|
+
}
|
|
429
|
+
let remoteFlow = null;
|
|
430
|
+
try {
|
|
431
|
+
remoteFlow = await getFlow(client, flowData.id);
|
|
432
|
+
}
|
|
433
|
+
catch (error) {
|
|
434
|
+
this.logger.verbose(`Could not GET flow ${flowIdn}: ${error.response?.status ?? error.message}`);
|
|
435
|
+
}
|
|
436
|
+
const counts = emptyFlowSyncCounts();
|
|
437
|
+
await syncFlowMetadata(client, flowData.id, localFlow, remoteFlow, false, counts);
|
|
438
|
+
const total = totalFlowSyncOps(counts);
|
|
439
|
+
if (total > 0) {
|
|
440
|
+
this.logger.info(`↑ Flow ${flowIdn}: ${describeFlowSyncCounts(counts)}`);
|
|
441
|
+
}
|
|
442
|
+
for (const err of counts.errors) {
|
|
443
|
+
this.logger.warn(err);
|
|
444
|
+
}
|
|
445
|
+
// Stamp the hash so the next push skips this file unless it changes again.
|
|
446
|
+
newHashes[change.path] = sha256(content);
|
|
447
|
+
return total;
|
|
448
|
+
}
|
|
384
449
|
/**
|
|
385
450
|
* Push a skill update
|
|
386
451
|
*/
|
|
@@ -488,6 +553,21 @@ export class ProjectSyncStrategy {
|
|
|
488
553
|
for (const [projectIdn, projectData] of Object.entries(mapData.projects)) {
|
|
489
554
|
for (const [agentIdn, agentData] of Object.entries(projectData.agents)) {
|
|
490
555
|
for (const [flowIdn, flowData] of Object.entries(agentData.flows)) {
|
|
556
|
+
// Flow metadata change detection (title/events/state_fields).
|
|
557
|
+
// Surfaced as a change so push() picks it up alongside skill edits.
|
|
558
|
+
const flowMetaPath = flowMetadataPath(customer.idn, projectIdn, agentIdn, flowIdn);
|
|
559
|
+
if (await fs.pathExists(flowMetaPath)) {
|
|
560
|
+
const content = await fs.readFile(flowMetaPath, 'utf8');
|
|
561
|
+
const currentHash = sha256(content);
|
|
562
|
+
const storedHash = hashes[flowMetaPath];
|
|
563
|
+
if (storedHash !== currentHash) {
|
|
564
|
+
changes.push({
|
|
565
|
+
item: {},
|
|
566
|
+
operation: 'modified',
|
|
567
|
+
path: flowMetaPath
|
|
568
|
+
});
|
|
569
|
+
}
|
|
570
|
+
}
|
|
491
571
|
for (const [skillIdn, _skillData] of Object.entries(flowData.skills)) {
|
|
492
572
|
const skillFile = await getSingleSkillFile(customer.idn, projectIdn, agentIdn, flowIdn, skillIdn);
|
|
493
573
|
if (skillFile) {
|
|
@@ -54,6 +54,17 @@ export declare class V2ProjectSyncStrategy implements ISyncStrategy<ProjectMeta,
|
|
|
54
54
|
*/
|
|
55
55
|
private pullLibrary;
|
|
56
56
|
push(customer: CustomerConfig, changes?: ChangeItem<LocalProjectData>[]): Promise<PushResult>;
|
|
57
|
+
/**
|
|
58
|
+
* Recognize the V2 flow YAML location: .../agents/{agent}/flows/{flow}/{flow}.yaml
|
|
59
|
+
* Distinguishes it from skill scripts, library YAMLs, and attribute files.
|
|
60
|
+
*/
|
|
61
|
+
private isV2FlowYamlPath;
|
|
62
|
+
/**
|
|
63
|
+
* Push V2 flow YAML changes. Closes GH issue #3 for newo_v2 layout.
|
|
64
|
+
* Parses the V2 YAML, converts to V1-shaped FlowMetadata, and reuses the
|
|
65
|
+
* shared syncFlowMetadata routine that calls PATCH/POST/DELETE per child.
|
|
66
|
+
*/
|
|
67
|
+
private pushV2FlowYamlUpdate;
|
|
57
68
|
/**
|
|
58
69
|
* Push a V2 skill update
|
|
59
70
|
*
|
|
@@ -14,12 +14,13 @@
|
|
|
14
14
|
* skills/{SkillIdn}.nsl|.nslg
|
|
15
15
|
*/
|
|
16
16
|
import fs from 'fs-extra';
|
|
17
|
-
import { listProjects, listAgents, listFlowSkills, listFlowEvents, listFlowStates, updateSkill, publishFlow, getProjectAttributes, getCustomerAttributes, listLibraries, updateLibrarySkill, } from '../../../api.js';
|
|
17
|
+
import { listProjects, listAgents, listFlowSkills, listFlowEvents, listFlowStates, updateSkill, publishFlow, getProjectAttributes, getCustomerAttributes, listLibraries, updateLibrarySkill, getFlow, } from '../../../api.js';
|
|
18
|
+
import { syncFlowMetadata, emptyFlowSyncCounts, totalFlowSyncOps, describeFlowSyncCounts } from '../../../sync/flow-metadata.js';
|
|
18
19
|
import { ensureStateOnly, writeFileSafe, mapPath, } from '../../../fsutil.js';
|
|
19
20
|
import { sha256, saveHashes, loadHashes } from '../../../hash.js';
|
|
20
21
|
import { v2ImportVersionPath, v2ProjectYamlPath, v2AgentYamlPath, v2FlowYamlPath, v2SkillScriptPath, v2SkillRelativePath, v2ProjectAttributesPath, v2CustomerAttributesPath, v2AkbDir, v2AkbPath, v2LibraryYamlPath, v2LibrarySkillScriptPath, v2LibrarySkillRelativePath, } from '../../../format/paths-v2.js';
|
|
21
22
|
import { V2_IMPORT_VERSION, } from '../../../format/types.js';
|
|
22
|
-
import { generateV2FlowYaml, generateV2ProjectYaml, generateV2AgentYaml, buildV2InlineSkill, buildV2FlowEvent, buildV2StateField, } from '../../../format/v2-yaml.js';
|
|
23
|
+
import { generateV2FlowYaml, generateV2ProjectYaml, generateV2AgentYaml, buildV2InlineSkill, buildV2FlowEvent, buildV2StateField, parseV2FlowYaml, } from '../../../format/v2-yaml.js';
|
|
23
24
|
import { isContentDifferent } from '../../../sync/skill-files.js';
|
|
24
25
|
import yaml from 'js-yaml';
|
|
25
26
|
import { patchYamlToPyyaml } from '../../../format/yaml-patch.js';
|
|
@@ -440,6 +441,14 @@ export class V2ProjectSyncStrategy {
|
|
|
440
441
|
for (const change of changes) {
|
|
441
442
|
try {
|
|
442
443
|
if (change.operation === 'modified') {
|
|
444
|
+
// V2 flow YAML: newo_customers/{cust}/{proj}/agents/{agent}/flows/{flow}/{flow}.yaml
|
|
445
|
+
// The flow YAML carries title, events, and state_fields inline, so
|
|
446
|
+
// changes there must sync to the platform like V1 metadata.yaml.
|
|
447
|
+
if (this.isV2FlowYamlPath(change.path)) {
|
|
448
|
+
const count = await this.pushV2FlowYamlUpdate(client, change, mapData, newHashes);
|
|
449
|
+
result.updated += count;
|
|
450
|
+
continue;
|
|
451
|
+
}
|
|
443
452
|
// Detect if this is a library skill or flow skill by path
|
|
444
453
|
const isLibrary = change.path.includes('/libraries/');
|
|
445
454
|
const count = isLibrary
|
|
@@ -458,6 +467,103 @@ export class V2ProjectSyncStrategy {
|
|
|
458
467
|
}
|
|
459
468
|
return result;
|
|
460
469
|
}
|
|
470
|
+
/**
|
|
471
|
+
* Recognize the V2 flow YAML location: .../agents/{agent}/flows/{flow}/{flow}.yaml
|
|
472
|
+
* Distinguishes it from skill scripts, library YAMLs, and attribute files.
|
|
473
|
+
*/
|
|
474
|
+
isV2FlowYamlPath(p) {
|
|
475
|
+
const parts = p.split('/');
|
|
476
|
+
const file = parts[parts.length - 1];
|
|
477
|
+
if (!file || !file.endsWith('.yaml'))
|
|
478
|
+
return false;
|
|
479
|
+
// .../agents/{agent}/flows/{flow}/{flow}.yaml → last 5 parts:
|
|
480
|
+
// agents, {agent}, flows, {flow}, {flow}.yaml
|
|
481
|
+
if (parts.length < 5)
|
|
482
|
+
return false;
|
|
483
|
+
const flowsKeyword = parts[parts.length - 3];
|
|
484
|
+
const agentsKeyword = parts[parts.length - 5];
|
|
485
|
+
const flowFolder = parts[parts.length - 2] || '';
|
|
486
|
+
const stem = file.slice(0, -'.yaml'.length);
|
|
487
|
+
return flowsKeyword === 'flows' && agentsKeyword === 'agents' && stem === flowFolder;
|
|
488
|
+
}
|
|
489
|
+
/**
|
|
490
|
+
* Push V2 flow YAML changes. Closes GH issue #3 for newo_v2 layout.
|
|
491
|
+
* Parses the V2 YAML, converts to V1-shaped FlowMetadata, and reuses the
|
|
492
|
+
* shared syncFlowMetadata routine that calls PATCH/POST/DELETE per child.
|
|
493
|
+
*/
|
|
494
|
+
async pushV2FlowYamlUpdate(client, change, mapData, newHashes) {
|
|
495
|
+
const parts = change.path.split('/');
|
|
496
|
+
const flowIdn = parts[parts.length - 2] || '';
|
|
497
|
+
const agentIdn = parts[parts.length - 4] || '';
|
|
498
|
+
const projectIdn = parts[parts.length - 6] || '';
|
|
499
|
+
const projectData = mapData.projects[projectIdn];
|
|
500
|
+
const agentData = projectData?.agents[agentIdn];
|
|
501
|
+
const flowData = agentData?.flows[flowIdn];
|
|
502
|
+
if (!flowData?.id) {
|
|
503
|
+
this.logger.warn(`[newo_v2] Flow YAML change but flow not in project map: ${projectIdn}/${agentIdn}/${flowIdn}`);
|
|
504
|
+
return 0;
|
|
505
|
+
}
|
|
506
|
+
const v2Flow = await parseV2FlowYaml(change.path);
|
|
507
|
+
// Convert V2 → V1-shaped FlowMetadata for the shared sync routine.
|
|
508
|
+
// V2 events lack `id` and `description`; we fill defaults so the shape
|
|
509
|
+
// matches FlowEvent[] / FlowState[] expected by syncFlowMetadata.
|
|
510
|
+
// Optional fields are omitted (not set to undefined) to satisfy
|
|
511
|
+
// exactOptionalPropertyTypes.
|
|
512
|
+
const localMeta = {
|
|
513
|
+
id: flowData.id,
|
|
514
|
+
idn: v2Flow.idn,
|
|
515
|
+
title: v2Flow.title,
|
|
516
|
+
description: v2Flow.description ?? '',
|
|
517
|
+
default_runner_type: v2Flow.default_runner_type || 'guidance',
|
|
518
|
+
default_model: {
|
|
519
|
+
provider_idn: v2Flow.default_provider_idn,
|
|
520
|
+
model_idn: v2Flow.default_model_idn,
|
|
521
|
+
},
|
|
522
|
+
events: (v2Flow.events || []).map((e) => {
|
|
523
|
+
const out = {
|
|
524
|
+
id: '',
|
|
525
|
+
idn: e.idn,
|
|
526
|
+
description: '',
|
|
527
|
+
skill_selector: e.skill_selector,
|
|
528
|
+
interrupt_mode: (e.interrupt_mode || 'queue'),
|
|
529
|
+
...(e.skill_idn != null ? { skill_idn: e.skill_idn } : {}),
|
|
530
|
+
...(e.state_idn != null ? { state_idn: e.state_idn } : {}),
|
|
531
|
+
...(e.integration_idn != null ? { integration_idn: e.integration_idn } : {}),
|
|
532
|
+
...(e.connector_idn != null ? { connector_idn: e.connector_idn } : {}),
|
|
533
|
+
};
|
|
534
|
+
return out;
|
|
535
|
+
}),
|
|
536
|
+
state_fields: (v2Flow.state_fields || []).map((s) => {
|
|
537
|
+
const out = {
|
|
538
|
+
id: '',
|
|
539
|
+
idn: s.idn,
|
|
540
|
+
title: s.title || s.idn,
|
|
541
|
+
scope: (s.scope || 'flow'),
|
|
542
|
+
...(s.default_value != null ? { default_value: s.default_value } : {}),
|
|
543
|
+
};
|
|
544
|
+
return out;
|
|
545
|
+
}),
|
|
546
|
+
};
|
|
547
|
+
let remoteFlow = null;
|
|
548
|
+
try {
|
|
549
|
+
remoteFlow = await getFlow(client, flowData.id);
|
|
550
|
+
}
|
|
551
|
+
catch (error) {
|
|
552
|
+
this.logger.verbose(`[newo_v2] Could not GET flow ${flowIdn}: ${error.response?.status ?? error.message}`);
|
|
553
|
+
}
|
|
554
|
+
const counts = emptyFlowSyncCounts();
|
|
555
|
+
await syncFlowMetadata(client, flowData.id, localMeta, remoteFlow, false, counts);
|
|
556
|
+
const total = totalFlowSyncOps(counts);
|
|
557
|
+
if (total > 0) {
|
|
558
|
+
this.logger.info(`[newo_v2] ↑ Flow ${flowIdn}: ${describeFlowSyncCounts(counts)}`);
|
|
559
|
+
}
|
|
560
|
+
for (const err of counts.errors) {
|
|
561
|
+
this.logger.warn(err);
|
|
562
|
+
}
|
|
563
|
+
const content = await fs.readFile(change.path, 'utf8');
|
|
564
|
+
newHashes[change.path] = sha256(content);
|
|
565
|
+
return total;
|
|
566
|
+
}
|
|
461
567
|
/**
|
|
462
568
|
* Push a V2 skill update
|
|
463
569
|
*
|
|
@@ -562,6 +668,23 @@ export class V2ProjectSyncStrategy {
|
|
|
562
668
|
for (const [projectIdn, projectData] of Object.entries(mapData.projects)) {
|
|
563
669
|
// Flow skills
|
|
564
670
|
for (const [agentIdn, agentData] of Object.entries(projectData.agents)) {
|
|
671
|
+
for (const [flowIdn, _flowData] of Object.entries(agentData.flows)) {
|
|
672
|
+
// V2 stores flow events / state_fields / title inline in the flow
|
|
673
|
+
// YAML. Detect changes here so push() can sync them (GH issue #3).
|
|
674
|
+
const flowYamlPath = v2FlowYamlPath(customer.idn, projectIdn, agentIdn, flowIdn);
|
|
675
|
+
if (await fs.pathExists(flowYamlPath)) {
|
|
676
|
+
const content = await fs.readFile(flowYamlPath, 'utf8');
|
|
677
|
+
const currentHash = sha256(content);
|
|
678
|
+
const storedHash = hashes[flowYamlPath];
|
|
679
|
+
if (storedHash !== currentHash) {
|
|
680
|
+
changes.push({
|
|
681
|
+
item: {},
|
|
682
|
+
operation: 'modified',
|
|
683
|
+
path: flowYamlPath
|
|
684
|
+
});
|
|
685
|
+
}
|
|
686
|
+
}
|
|
687
|
+
}
|
|
565
688
|
for (const [flowIdn, flowData] of Object.entries(agentData.flows)) {
|
|
566
689
|
for (const [skillIdn, skillMeta] of Object.entries(flowData.skills)) {
|
|
567
690
|
const scriptPath = v2SkillScriptPath(customer.idn, projectIdn, agentIdn, flowIdn, skillIdn, skillMeta.runner_type);
|
package/dist/sync/attributes.js
CHANGED
|
@@ -7,6 +7,7 @@ import path from 'path';
|
|
|
7
7
|
import fs from 'fs-extra';
|
|
8
8
|
import yaml from 'js-yaml';
|
|
9
9
|
import { patchYamlToPyyaml } from '../format/yaml-patch.js';
|
|
10
|
+
import { isJsonValueType, normalizeJsonValueForStorage, jsonValuesEqual } from './json-attr-utils.js';
|
|
10
11
|
/**
|
|
11
12
|
* Save customer attributes to YAML format and return content for hashing
|
|
12
13
|
*/
|
|
@@ -28,16 +29,23 @@ export async function saveCustomerAttributes(client, customer, verbose = false)
|
|
|
28
29
|
if (attr.id) {
|
|
29
30
|
idMapping[attr.idn] = attr.id;
|
|
30
31
|
}
|
|
31
|
-
//
|
|
32
|
+
// Coerce JSON-typed values to a STRING. The API can return the value
|
|
33
|
+
// as a parsed object for `value_type: json` attributes; if we let
|
|
34
|
+
// yaml.dump serialize that as a YAML structure the next push sends
|
|
35
|
+
// `{"value": {...}}` instead of `{"value": "..."}` and the Workflow
|
|
36
|
+
// Builder canvas breaks. See src/sync/json-attr-utils.ts for the
|
|
37
|
+
// full rationale.
|
|
32
38
|
let processedValue = attr.value;
|
|
33
|
-
if (
|
|
39
|
+
if (isJsonValueType(attr.value_type)) {
|
|
40
|
+
processedValue = normalizeJsonValueForStorage(attr.value);
|
|
41
|
+
}
|
|
42
|
+
else if (typeof attr.value === 'string' && attr.value.startsWith('[{') && attr.value.endsWith('}]')) {
|
|
43
|
+
// Legacy: reformat array-of-objects JSON strings for readability
|
|
34
44
|
try {
|
|
35
|
-
// Parse and reformat JSON for better readability
|
|
36
45
|
const parsed = JSON.parse(attr.value);
|
|
37
|
-
processedValue = JSON.stringify(parsed, null, 0); //
|
|
46
|
+
processedValue = JSON.stringify(parsed, null, 0); // compact, valid JSON
|
|
38
47
|
}
|
|
39
48
|
catch (e) {
|
|
40
|
-
// Keep original if parsing fails
|
|
41
49
|
processedValue = attr.value;
|
|
42
50
|
}
|
|
43
51
|
}
|
|
@@ -114,9 +122,13 @@ export async function saveProjectAttributes(client, customer, projectId, project
|
|
|
114
122
|
if (attr.id) {
|
|
115
123
|
idMapping[attr.idn] = attr.id;
|
|
116
124
|
}
|
|
117
|
-
//
|
|
125
|
+
// Coerce JSON-typed values to a STRING. See json-attr-utils.ts for
|
|
126
|
+
// why this matters (Workflow Builder canvas blank-screen bug).
|
|
118
127
|
let processedValue = attr.value;
|
|
119
|
-
if (
|
|
128
|
+
if (isJsonValueType(attr.value_type)) {
|
|
129
|
+
processedValue = normalizeJsonValueForStorage(attr.value);
|
|
130
|
+
}
|
|
131
|
+
else if (typeof attr.value === 'string' && attr.value.startsWith('[{') && attr.value.endsWith('}]')) {
|
|
120
132
|
try {
|
|
121
133
|
const parsed = JSON.parse(attr.value);
|
|
122
134
|
processedValue = JSON.stringify(parsed, null, 0);
|
|
@@ -243,17 +255,31 @@ export async function pushProjectAttributes(client, customer, projectId, project
|
|
|
243
255
|
}
|
|
244
256
|
// Value type is already parsed (we removed !enum tags above)
|
|
245
257
|
const valueType = localAttr.value_type;
|
|
246
|
-
|
|
247
|
-
|
|
248
|
-
|
|
249
|
-
|
|
258
|
+
const isJson = isJsonValueType(valueType);
|
|
259
|
+
// Check if value changed.
|
|
260
|
+
// For JSON-typed values, compare canonical (compact) JSON so that
|
|
261
|
+
// pretty- vs compact-printed forms don't register as changes and
|
|
262
|
+
// string vs object representations compare equal. For everything
|
|
263
|
+
// else, fall back to the existing String() comparison (which still
|
|
264
|
+
// preserves 0, false, "" via ??).
|
|
265
|
+
const valuesAreEqual = isJson
|
|
266
|
+
? jsonValuesEqual(localAttr.value, remoteAttr.value)
|
|
267
|
+
: String(localAttr.value ?? '') === String(remoteAttr.value ?? '');
|
|
268
|
+
if (!valuesAreEqual) {
|
|
250
269
|
if (verbose)
|
|
251
270
|
console.log(` 🔄 Updating project attribute: ${localAttr.idn}`);
|
|
252
271
|
try {
|
|
272
|
+
// Always send JSON-typed values as a STRING. If the API or our
|
|
273
|
+
// YAML loader handed us an object, the platform stores it
|
|
274
|
+
// differently from the original string and the Workflow Builder
|
|
275
|
+
// canvas blanks out.
|
|
276
|
+
const valueToSend = isJson
|
|
277
|
+
? normalizeJsonValueForStorage(localAttr.value)
|
|
278
|
+
: localAttr.value;
|
|
253
279
|
const attributeToUpdate = {
|
|
254
280
|
id: attributeId,
|
|
255
281
|
idn: localAttr.idn,
|
|
256
|
-
value:
|
|
282
|
+
value: valueToSend,
|
|
257
283
|
title: localAttr.title,
|
|
258
284
|
description: localAttr.description,
|
|
259
285
|
group: localAttr.group,
|