newo 3.7.3 → 3.7.5
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 +35 -2
- package/README.md +71 -2
- package/dist/cli/commands/get-skill.d.ts +3 -0
- package/dist/cli/commands/get-skill.js +72 -0
- package/dist/cli/commands/help.js +29 -0
- package/dist/cli/commands/logs.d.ts +6 -1
- package/dist/cli/commands/logs.js +62 -14
- package/dist/cli/commands/sandbox.d.ts +10 -4
- package/dist/cli/commands/sandbox.js +182 -51
- package/dist/cli/commands/update-skill.d.ts +4 -0
- package/dist/cli/commands/update-skill.js +119 -0
- package/dist/cli.js +8 -0
- package/dist/domain/strategies/sync/V2ProjectSyncStrategy.d.ts +37 -0
- package/dist/domain/strategies/sync/V2ProjectSyncStrategy.js +383 -24
- package/dist/sandbox/chat.d.ts +23 -3
- package/dist/sandbox/chat.js +83 -30
- package/dist/sync/remote-skill.d.ts +33 -0
- package/dist/sync/remote-skill.js +52 -0
- package/package.json +1 -1
- package/src/cli/commands/get-skill.ts +84 -0
- package/src/cli/commands/help.ts +29 -0
- package/src/cli/commands/logs.ts +83 -15
- package/src/cli/commands/sandbox.ts +238 -60
- package/src/cli/commands/update-skill.ts +139 -0
- package/src/cli.ts +10 -0
- package/src/domain/strategies/sync/V2ProjectSyncStrategy.ts +530 -26
- package/src/sandbox/chat.ts +106 -29
- package/src/sync/remote-skill.ts +92 -0
|
@@ -40,12 +40,15 @@ import type {
|
|
|
40
40
|
} from '../../../types.js';
|
|
41
41
|
import type { LocalProjectData, LocalAgentData, LocalFlowData, LocalSkillData, ApiClientFactory } from './ProjectSyncStrategy.js';
|
|
42
42
|
import fs from 'fs-extra';
|
|
43
|
+
import path from 'path';
|
|
43
44
|
import {
|
|
44
45
|
listProjects,
|
|
45
46
|
listAgents,
|
|
46
47
|
listFlowSkills,
|
|
47
48
|
listFlowEvents,
|
|
48
49
|
listFlowStates,
|
|
50
|
+
createSkill,
|
|
51
|
+
createSkillParameter,
|
|
49
52
|
updateSkill,
|
|
50
53
|
publishFlow,
|
|
51
54
|
getProjectAttributes,
|
|
@@ -70,6 +73,7 @@ import { sha256, saveHashes, loadHashes } from '../../../hash.js';
|
|
|
70
73
|
import {
|
|
71
74
|
v2ImportVersionPath,
|
|
72
75
|
v2ProjectYamlPath,
|
|
76
|
+
v2AgentDir,
|
|
73
77
|
v2AgentYamlPath,
|
|
74
78
|
v2FlowYamlPath,
|
|
75
79
|
v2SkillScriptPath,
|
|
@@ -89,10 +93,10 @@ import {
|
|
|
89
93
|
generateV2FlowYaml,
|
|
90
94
|
generateV2ProjectYaml,
|
|
91
95
|
generateV2AgentYaml,
|
|
96
|
+
parseV2FlowYaml,
|
|
92
97
|
buildV2InlineSkill,
|
|
93
98
|
buildV2FlowEvent,
|
|
94
99
|
buildV2StateField,
|
|
95
|
-
parseV2FlowYaml,
|
|
96
100
|
type V2InlineSkill,
|
|
97
101
|
type V2FlowEvent,
|
|
98
102
|
type V2StateField,
|
|
@@ -100,6 +104,15 @@ import {
|
|
|
100
104
|
import { isContentDifferent } from '../../../sync/skill-files.js';
|
|
101
105
|
import yaml from 'js-yaml';
|
|
102
106
|
import { patchYamlToPyyaml } from '../../../format/yaml-patch.js';
|
|
107
|
+
import type { RunnerType, SkillParameter } from '../../../types.js';
|
|
108
|
+
|
|
109
|
+
interface V2FlowSkillTarget {
|
|
110
|
+
projectIdn: string;
|
|
111
|
+
agentIdn: string;
|
|
112
|
+
flowIdn: string;
|
|
113
|
+
skillIdn: string;
|
|
114
|
+
skillData: SkillMetadata | undefined;
|
|
115
|
+
}
|
|
103
116
|
|
|
104
117
|
/**
|
|
105
118
|
* V2ProjectSyncStrategy - same API, newo_v2 file layout
|
|
@@ -655,9 +668,17 @@ export class V2ProjectSyncStrategy implements ISyncStrategy<ProjectMeta, LocalPr
|
|
|
655
668
|
}
|
|
656
669
|
|
|
657
670
|
const mapData = await fs.readJson(mapFile) as ProjectMap;
|
|
671
|
+
const metadataSync = await this.syncV2FlowYamlDefinitions(client, customer, mapData, newHashes);
|
|
672
|
+
result.created += metadataSync.created;
|
|
673
|
+
result.updated += metadataSync.updated;
|
|
674
|
+
result.errors.push(...metadataSync.errors);
|
|
658
675
|
|
|
659
676
|
for (const change of changes) {
|
|
660
677
|
try {
|
|
678
|
+
if (metadataSync.syncedPaths.has(change.path)) {
|
|
679
|
+
continue;
|
|
680
|
+
}
|
|
681
|
+
|
|
661
682
|
if (change.operation === 'modified') {
|
|
662
683
|
// V2 flow YAML: newo_customers/{cust}/{proj}/agents/{agent}/flows/{flow}/{flow}.yaml
|
|
663
684
|
// The flow YAML carries title, events, and state_fields inline, so
|
|
@@ -672,7 +693,7 @@ export class V2ProjectSyncStrategy implements ISyncStrategy<ProjectMeta, LocalPr
|
|
|
672
693
|
const isLibrary = change.path.includes('/libraries/');
|
|
673
694
|
const count = isLibrary
|
|
674
695
|
? await this.pushV2LibrarySkillUpdate(client, change, mapData, newHashes)
|
|
675
|
-
: await this.pushV2SkillUpdate(client, change, mapData, newHashes);
|
|
696
|
+
: await this.pushV2SkillUpdate(client, change, mapData, newHashes, customer.idn);
|
|
676
697
|
result.updated += count;
|
|
677
698
|
}
|
|
678
699
|
} catch (error) {
|
|
@@ -682,6 +703,10 @@ export class V2ProjectSyncStrategy implements ISyncStrategy<ProjectMeta, LocalPr
|
|
|
682
703
|
}
|
|
683
704
|
}
|
|
684
705
|
|
|
706
|
+
if (metadataSync.created > 0 || metadataSync.updated > 0) {
|
|
707
|
+
await writeFileSafe(mapFile, JSON.stringify(mapData, null, 2));
|
|
708
|
+
}
|
|
709
|
+
|
|
685
710
|
await saveHashes(newHashes, customer.idn);
|
|
686
711
|
|
|
687
712
|
if (result.created > 0 || result.updated > 0) {
|
|
@@ -800,6 +825,321 @@ export class V2ProjectSyncStrategy implements ISyncStrategy<ProjectMeta, LocalPr
|
|
|
800
825
|
return total;
|
|
801
826
|
}
|
|
802
827
|
|
|
828
|
+
/**
|
|
829
|
+
* Reconcile inline skill definitions from V2 flow YAML before pushing scripts.
|
|
830
|
+
*
|
|
831
|
+
* V2 keeps skill metadata (model, runner_type, parameters) in the flow YAML,
|
|
832
|
+
* not in a separate skill metadata file. The map only contains the remote IDs
|
|
833
|
+
* from a previous pull, so new local skills must be created before their
|
|
834
|
+
* callers can be published.
|
|
835
|
+
*/
|
|
836
|
+
private async syncV2FlowYamlDefinitions(
|
|
837
|
+
client: AxiosInstance,
|
|
838
|
+
customer: CustomerConfig,
|
|
839
|
+
mapData: ProjectMap,
|
|
840
|
+
newHashes: HashStore
|
|
841
|
+
): Promise<{ created: number; updated: number; syncedPaths: Set<string>; errors: string[] }> {
|
|
842
|
+
let created = 0;
|
|
843
|
+
let updated = 0;
|
|
844
|
+
const syncedPaths = new Set<string>();
|
|
845
|
+
const errors: string[] = [];
|
|
846
|
+
|
|
847
|
+
for (const [projectIdn, projectData] of Object.entries(mapData.projects)) {
|
|
848
|
+
for (const [agentIdn, agentData] of Object.entries(projectData.agents)) {
|
|
849
|
+
for (const [flowIdn, flowData] of Object.entries(agentData.flows)) {
|
|
850
|
+
const flowYamlPath = v2FlowYamlPath(customer.idn, projectIdn, agentIdn, flowIdn);
|
|
851
|
+
if (!(await fs.pathExists(flowYamlPath))) {
|
|
852
|
+
continue;
|
|
853
|
+
}
|
|
854
|
+
|
|
855
|
+
let flowDef;
|
|
856
|
+
try {
|
|
857
|
+
flowDef = await parseV2FlowYaml(flowYamlPath);
|
|
858
|
+
} catch (error) {
|
|
859
|
+
this.logger.warn(
|
|
860
|
+
`[newo_v2] Failed to parse flow YAML ${flowYamlPath}: ${error instanceof Error ? error.message : String(error)}`
|
|
861
|
+
);
|
|
862
|
+
continue;
|
|
863
|
+
}
|
|
864
|
+
|
|
865
|
+
for (const skill of flowDef.skills || []) {
|
|
866
|
+
const skillLocator = `${projectIdn}/${agentIdn}/${flowIdn}/${skill.idn}`;
|
|
867
|
+
// Per-skill failure isolation: one broken skill must not abort the
|
|
868
|
+
// push of every other project/flow in the workspace.
|
|
869
|
+
try {
|
|
870
|
+
const runnerType = this.normalizeRunnerType(skill.runner_type);
|
|
871
|
+
const scriptPath = await this.resolveV2FlowSkillScriptPath(
|
|
872
|
+
customer.idn,
|
|
873
|
+
projectIdn,
|
|
874
|
+
agentIdn,
|
|
875
|
+
flowIdn,
|
|
876
|
+
skill.idn,
|
|
877
|
+
runnerType,
|
|
878
|
+
skill.prompt_script
|
|
879
|
+
);
|
|
880
|
+
|
|
881
|
+
if (!(await fs.pathExists(scriptPath))) {
|
|
882
|
+
errors.push(
|
|
883
|
+
`[newo_v2] Missing script for skill ${skillLocator}: ${scriptPath}`
|
|
884
|
+
);
|
|
885
|
+
continue;
|
|
886
|
+
}
|
|
887
|
+
|
|
888
|
+
const content = await fs.readFile(scriptPath, 'utf8');
|
|
889
|
+
const localMetadata = this.buildV2SkillMetadataFromYaml(skill, flowDef, runnerType, flowData.skills[skill.idn]);
|
|
890
|
+
const existingSkill = flowData.skills[skill.idn];
|
|
891
|
+
|
|
892
|
+
if (!existingSkill) {
|
|
893
|
+
this.assertSkillModelResolved(localMetadata, skillLocator);
|
|
894
|
+
try {
|
|
895
|
+
const createdSkill = await createSkill(client, flowData.id, {
|
|
896
|
+
idn: localMetadata.idn,
|
|
897
|
+
title: localMetadata.title,
|
|
898
|
+
prompt_script: content,
|
|
899
|
+
runner_type: localMetadata.runner_type,
|
|
900
|
+
model: localMetadata.model,
|
|
901
|
+
parameters: localMetadata.parameters,
|
|
902
|
+
path: localMetadata.path || ''
|
|
903
|
+
});
|
|
904
|
+
|
|
905
|
+
// The create endpoint ignores inline `parameters` (verified
|
|
906
|
+
// against the live platform) — create them explicitly.
|
|
907
|
+
await this.createMissingSkillParameters(
|
|
908
|
+
client,
|
|
909
|
+
{ ...localMetadata, id: createdSkill.id, parameters: [] },
|
|
910
|
+
localMetadata
|
|
911
|
+
);
|
|
912
|
+
|
|
913
|
+
flowData.skills[skill.idn] = {
|
|
914
|
+
...localMetadata,
|
|
915
|
+
id: createdSkill.id
|
|
916
|
+
};
|
|
917
|
+
newHashes[scriptPath] = sha256(content);
|
|
918
|
+
syncedPaths.add(scriptPath);
|
|
919
|
+
created++;
|
|
920
|
+
this.logger.info(`[newo_v2] Created skill: ${flowIdn}/${skill.idn}`);
|
|
921
|
+
} catch (error) {
|
|
922
|
+
if (!this.isAlreadyExistsApiError(error)) {
|
|
923
|
+
throw error;
|
|
924
|
+
}
|
|
925
|
+
|
|
926
|
+
const remoteSkills = await listFlowSkills(client, flowData.id);
|
|
927
|
+
const remoteSkill = remoteSkills.find(s => s.idn === skill.idn);
|
|
928
|
+
if (!remoteSkill) {
|
|
929
|
+
throw error;
|
|
930
|
+
}
|
|
931
|
+
|
|
932
|
+
const remoteMetadata: SkillMetadata = {
|
|
933
|
+
id: remoteSkill.id,
|
|
934
|
+
idn: remoteSkill.idn,
|
|
935
|
+
title: remoteSkill.title,
|
|
936
|
+
runner_type: remoteSkill.runner_type,
|
|
937
|
+
model: remoteSkill.model,
|
|
938
|
+
parameters: this.normalizeParameters(remoteSkill.parameters),
|
|
939
|
+
path: remoteSkill.path
|
|
940
|
+
};
|
|
941
|
+
await this.createMissingSkillParameters(client, remoteMetadata, localMetadata);
|
|
942
|
+
await updateSkill(client, {
|
|
943
|
+
id: remoteSkill.id,
|
|
944
|
+
title: localMetadata.title,
|
|
945
|
+
idn: localMetadata.idn,
|
|
946
|
+
prompt_script: content,
|
|
947
|
+
runner_type: localMetadata.runner_type,
|
|
948
|
+
model: localMetadata.model,
|
|
949
|
+
parameters: localMetadata.parameters,
|
|
950
|
+
path: remoteSkill.path || localMetadata.path
|
|
951
|
+
});
|
|
952
|
+
|
|
953
|
+
flowData.skills[skill.idn] = {
|
|
954
|
+
...localMetadata,
|
|
955
|
+
id: remoteSkill.id,
|
|
956
|
+
path: remoteSkill.path || localMetadata.path
|
|
957
|
+
};
|
|
958
|
+
newHashes[scriptPath] = sha256(content);
|
|
959
|
+
syncedPaths.add(scriptPath);
|
|
960
|
+
updated++;
|
|
961
|
+
this.logger.info(`[newo_v2] Reused existing skill: ${flowIdn}/${skill.idn}`);
|
|
962
|
+
}
|
|
963
|
+
continue;
|
|
964
|
+
}
|
|
965
|
+
|
|
966
|
+
const createdParameters = await this.createMissingSkillParameters(client, existingSkill, localMetadata);
|
|
967
|
+
|
|
968
|
+
if (createdParameters > 0 || this.skillMetadataDiffers(existingSkill, localMetadata)) {
|
|
969
|
+
this.assertSkillModelResolved(localMetadata, skillLocator);
|
|
970
|
+
await updateSkill(client, {
|
|
971
|
+
id: existingSkill.id,
|
|
972
|
+
title: localMetadata.title,
|
|
973
|
+
idn: localMetadata.idn,
|
|
974
|
+
prompt_script: content,
|
|
975
|
+
runner_type: localMetadata.runner_type,
|
|
976
|
+
model: localMetadata.model,
|
|
977
|
+
parameters: localMetadata.parameters,
|
|
978
|
+
path: localMetadata.path
|
|
979
|
+
});
|
|
980
|
+
|
|
981
|
+
flowData.skills[skill.idn] = {
|
|
982
|
+
...localMetadata,
|
|
983
|
+
id: existingSkill.id
|
|
984
|
+
};
|
|
985
|
+
newHashes[scriptPath] = sha256(content);
|
|
986
|
+
syncedPaths.add(scriptPath);
|
|
987
|
+
updated++;
|
|
988
|
+
this.logger.info(`[newo_v2] Updated skill metadata: ${flowIdn}/${skill.idn}`);
|
|
989
|
+
}
|
|
990
|
+
} catch (error) {
|
|
991
|
+
errors.push(
|
|
992
|
+
`Failed to sync skill ${skillLocator}: ${error instanceof Error ? error.message : String(error)}`
|
|
993
|
+
);
|
|
994
|
+
}
|
|
995
|
+
}
|
|
996
|
+
}
|
|
997
|
+
}
|
|
998
|
+
}
|
|
999
|
+
|
|
1000
|
+
return { created, updated, syncedPaths, errors };
|
|
1001
|
+
}
|
|
1002
|
+
|
|
1003
|
+
private async createMissingSkillParameters(
|
|
1004
|
+
client: AxiosInstance,
|
|
1005
|
+
existing: SkillMetadata,
|
|
1006
|
+
local: SkillMetadata
|
|
1007
|
+
): Promise<number> {
|
|
1008
|
+
const existingNames = new Set(this.normalizeParameters(existing.parameters).map(p => p.name));
|
|
1009
|
+
let created = 0;
|
|
1010
|
+
|
|
1011
|
+
for (const parameter of local.parameters) {
|
|
1012
|
+
if (existingNames.has(parameter.name)) {
|
|
1013
|
+
continue;
|
|
1014
|
+
}
|
|
1015
|
+
|
|
1016
|
+
try {
|
|
1017
|
+
await createSkillParameter(client, existing.id, {
|
|
1018
|
+
name: parameter.name,
|
|
1019
|
+
default_value: parameter.default_value ?? ''
|
|
1020
|
+
});
|
|
1021
|
+
created++;
|
|
1022
|
+
this.logger.info(`[newo_v2] Created skill parameter: ${local.idn}/${parameter.name}`);
|
|
1023
|
+
} catch (error) {
|
|
1024
|
+
if (!this.isAlreadyExistsApiError(error)) {
|
|
1025
|
+
throw error;
|
|
1026
|
+
}
|
|
1027
|
+
}
|
|
1028
|
+
existingNames.add(parameter.name);
|
|
1029
|
+
}
|
|
1030
|
+
|
|
1031
|
+
return created;
|
|
1032
|
+
}
|
|
1033
|
+
|
|
1034
|
+
/**
|
|
1035
|
+
* Detect "resource already exists" API errors.
|
|
1036
|
+
*
|
|
1037
|
+
* Matches only on the precise phrases the platform actually returns
|
|
1038
|
+
* ("already exists", "duplicate key"). Loose substrings like "exist"
|
|
1039
|
+
* would otherwise sweep up unrelated "does not exist" / "doesn't exist"
|
|
1040
|
+
* errors and trigger an incorrect reuse fallback.
|
|
1041
|
+
*/
|
|
1042
|
+
private isAlreadyExistsApiError(error: unknown): boolean {
|
|
1043
|
+
const response = (error as { response?: { status?: number; data?: unknown } } | null | undefined)?.response;
|
|
1044
|
+
const status = response?.status;
|
|
1045
|
+
if (status !== 400 && status !== 409 && status !== 422) {
|
|
1046
|
+
return false;
|
|
1047
|
+
}
|
|
1048
|
+
|
|
1049
|
+
const haystack = JSON.stringify(
|
|
1050
|
+
response?.data ?? (error instanceof Error ? error.message : String(error))
|
|
1051
|
+
).toLowerCase();
|
|
1052
|
+
|
|
1053
|
+
return haystack.includes('already exists') || haystack.includes('duplicate key');
|
|
1054
|
+
}
|
|
1055
|
+
|
|
1056
|
+
private normalizeRunnerType(runnerType: string | undefined): RunnerType {
|
|
1057
|
+
return runnerType === 'nsl' ? 'nsl' : 'guidance';
|
|
1058
|
+
}
|
|
1059
|
+
|
|
1060
|
+
private normalizeParameters(parameters: readonly SkillParameter[] | undefined): SkillParameter[] {
|
|
1061
|
+
return (parameters || []).map(p => ({
|
|
1062
|
+
name: p.name,
|
|
1063
|
+
default_value: p.default_value ?? ''
|
|
1064
|
+
}));
|
|
1065
|
+
}
|
|
1066
|
+
|
|
1067
|
+
/**
|
|
1068
|
+
* Fail fast if no model could be resolved for a V2 skill.
|
|
1069
|
+
*
|
|
1070
|
+
* `buildV2SkillMetadataFromYaml` falls back to empty strings when neither
|
|
1071
|
+
* the skill nor the flow declare a model. The platform rejects empty
|
|
1072
|
+
* model_idn/provider_idn at creation/update time, but the error it returns
|
|
1073
|
+
* is generic — we surface a clearer message before issuing the request.
|
|
1074
|
+
*/
|
|
1075
|
+
private assertSkillModelResolved(metadata: SkillMetadata, locator: string): void {
|
|
1076
|
+
if (!metadata.model.model_idn || !metadata.model.provider_idn) {
|
|
1077
|
+
throw new Error(
|
|
1078
|
+
`[newo_v2] Cannot resolve model for skill ${locator}: ` +
|
|
1079
|
+
`model_idn="${metadata.model.model_idn}", provider_idn="${metadata.model.provider_idn}". ` +
|
|
1080
|
+
`Set either skill.model.* or flow default_model_idn/default_provider_idn in the flow YAML.`
|
|
1081
|
+
);
|
|
1082
|
+
}
|
|
1083
|
+
}
|
|
1084
|
+
|
|
1085
|
+
private buildV2SkillMetadataFromYaml(
|
|
1086
|
+
skill: V2InlineSkill,
|
|
1087
|
+
flowDef: Awaited<ReturnType<typeof parseV2FlowYaml>>,
|
|
1088
|
+
runnerType: RunnerType,
|
|
1089
|
+
existing?: SkillMetadata
|
|
1090
|
+
): SkillMetadata {
|
|
1091
|
+
return {
|
|
1092
|
+
id: existing?.id || '',
|
|
1093
|
+
idn: skill.idn,
|
|
1094
|
+
title: skill.title || '',
|
|
1095
|
+
runner_type: runnerType,
|
|
1096
|
+
model: {
|
|
1097
|
+
model_idn: skill.model?.model_idn || flowDef.default_model_idn || '',
|
|
1098
|
+
provider_idn: skill.model?.provider_idn || flowDef.default_provider_idn || ''
|
|
1099
|
+
},
|
|
1100
|
+
parameters: this.normalizeParameters(skill.parameters),
|
|
1101
|
+
path: existing?.path || ''
|
|
1102
|
+
};
|
|
1103
|
+
}
|
|
1104
|
+
|
|
1105
|
+
private skillMetadataDiffers(existing: SkillMetadata, local: SkillMetadata): boolean {
|
|
1106
|
+
// Compare model/parameters field-by-field, never via JSON.stringify of the
|
|
1107
|
+
// raw objects: the map stores model keys in platform API order
|
|
1108
|
+
// (provider_idn first) while YAML-built metadata uses model_idn first, and
|
|
1109
|
+
// a key-order-sensitive comparison flags every skill as changed.
|
|
1110
|
+
const paramsKey = (params: readonly SkillParameter[] | undefined): string =>
|
|
1111
|
+
JSON.stringify(
|
|
1112
|
+
this.normalizeParameters(params).sort((a, b) => a.name.localeCompare(b.name))
|
|
1113
|
+
);
|
|
1114
|
+
|
|
1115
|
+
return (
|
|
1116
|
+
existing.title !== local.title ||
|
|
1117
|
+
existing.runner_type !== local.runner_type ||
|
|
1118
|
+
existing.model.model_idn !== local.model.model_idn ||
|
|
1119
|
+
existing.model.provider_idn !== local.model.provider_idn ||
|
|
1120
|
+
paramsKey(existing.parameters) !== paramsKey(local.parameters)
|
|
1121
|
+
);
|
|
1122
|
+
}
|
|
1123
|
+
|
|
1124
|
+
private async resolveV2FlowSkillScriptPath(
|
|
1125
|
+
customerIdn: string,
|
|
1126
|
+
projectIdn: string,
|
|
1127
|
+
agentIdn: string,
|
|
1128
|
+
flowIdn: string,
|
|
1129
|
+
skillIdn: string,
|
|
1130
|
+
runnerType: RunnerType,
|
|
1131
|
+
promptScript?: string
|
|
1132
|
+
): Promise<string> {
|
|
1133
|
+
if (promptScript) {
|
|
1134
|
+
const fromPromptScript = `${v2AgentDir(customerIdn, projectIdn, agentIdn)}/${promptScript}`;
|
|
1135
|
+
if (await fs.pathExists(fromPromptScript)) {
|
|
1136
|
+
return fromPromptScript;
|
|
1137
|
+
}
|
|
1138
|
+
}
|
|
1139
|
+
|
|
1140
|
+
return v2SkillScriptPath(customerIdn, projectIdn, agentIdn, flowIdn, skillIdn, runnerType);
|
|
1141
|
+
}
|
|
1142
|
+
|
|
803
1143
|
/**
|
|
804
1144
|
* Push a V2 skill update
|
|
805
1145
|
*
|
|
@@ -809,26 +1149,16 @@ export class V2ProjectSyncStrategy implements ISyncStrategy<ProjectMeta, LocalPr
|
|
|
809
1149
|
client: AxiosInstance,
|
|
810
1150
|
change: ChangeItem<LocalProjectData>,
|
|
811
1151
|
mapData: ProjectMap,
|
|
812
|
-
newHashes: HashStore
|
|
1152
|
+
newHashes: HashStore,
|
|
1153
|
+
customerIdn: string
|
|
813
1154
|
): Promise<number> {
|
|
814
|
-
|
|
815
|
-
|
|
816
|
-
|
|
817
|
-
const
|
|
818
|
-
const skillIdn = skillFileName.replace(/\.(nsl|nslg|jinja|guidance)$/, '');
|
|
819
|
-
// skills/ -> flow/ -> flows/ -> agent/ -> agents/ -> project/
|
|
820
|
-
const flowIdn = pathParts[pathParts.length - 3] || '';
|
|
821
|
-
const agentIdn = pathParts[pathParts.length - 5] || '';
|
|
822
|
-
const projectIdn = pathParts[pathParts.length - 7] || '';
|
|
1155
|
+
const target =
|
|
1156
|
+
await this.resolveV2SkillTargetForScriptPath(customerIdn, change.path, mapData) ||
|
|
1157
|
+
this.resolveV2SkillTargetFromCanonicalPath(change.path, mapData);
|
|
1158
|
+
const skillData = target?.skillData;
|
|
823
1159
|
|
|
824
|
-
|
|
825
|
-
|
|
826
|
-
const agentData = projectData?.agents[agentIdn];
|
|
827
|
-
const flowData = agentData?.flows[flowIdn];
|
|
828
|
-
const skillData = flowData?.skills[skillIdn];
|
|
829
|
-
|
|
830
|
-
if (!skillData) {
|
|
831
|
-
throw new Error(`Skill ${skillIdn} not found in project map (path: ${change.path})`);
|
|
1160
|
+
if (!target || !skillData) {
|
|
1161
|
+
throw new Error(`Skill not found in project map (path: ${change.path})`);
|
|
832
1162
|
}
|
|
833
1163
|
|
|
834
1164
|
// Read updated script content
|
|
@@ -847,10 +1177,92 @@ export class V2ProjectSyncStrategy implements ISyncStrategy<ProjectMeta, LocalPr
|
|
|
847
1177
|
});
|
|
848
1178
|
|
|
849
1179
|
newHashes[change.path] = sha256(content);
|
|
850
|
-
this.logger.info(`[newo_v2] Pushed: ${skillIdn}`);
|
|
1180
|
+
this.logger.info(`[newo_v2] Pushed: ${target.skillIdn}`);
|
|
851
1181
|
return 1;
|
|
852
1182
|
}
|
|
853
1183
|
|
|
1184
|
+
private normalizePathForComparison(filePath: string): string {
|
|
1185
|
+
return path.resolve(filePath).replace(/\\/g, '/');
|
|
1186
|
+
}
|
|
1187
|
+
|
|
1188
|
+
private async resolveV2SkillTargetForScriptPath(
|
|
1189
|
+
customerIdn: string,
|
|
1190
|
+
scriptPath: string,
|
|
1191
|
+
mapData: ProjectMap
|
|
1192
|
+
): Promise<V2FlowSkillTarget | null> {
|
|
1193
|
+
const normalizedScriptPath = this.normalizePathForComparison(scriptPath);
|
|
1194
|
+
|
|
1195
|
+
for (const [projectIdn, projectData] of Object.entries(mapData.projects)) {
|
|
1196
|
+
for (const [agentIdn, agentData] of Object.entries(projectData.agents)) {
|
|
1197
|
+
for (const [flowIdn, flowData] of Object.entries(agentData.flows)) {
|
|
1198
|
+
const flowYamlPath = v2FlowYamlPath(customerIdn, projectIdn, agentIdn, flowIdn);
|
|
1199
|
+
if (!(await fs.pathExists(flowYamlPath))) {
|
|
1200
|
+
continue;
|
|
1201
|
+
}
|
|
1202
|
+
|
|
1203
|
+
let flowDef;
|
|
1204
|
+
try {
|
|
1205
|
+
flowDef = await parseV2FlowYaml(flowYamlPath);
|
|
1206
|
+
} catch {
|
|
1207
|
+
continue;
|
|
1208
|
+
}
|
|
1209
|
+
|
|
1210
|
+
for (const skill of flowDef.skills || []) {
|
|
1211
|
+
const runnerType = this.normalizeRunnerType(skill.runner_type || flowData.skills[skill.idn]?.runner_type);
|
|
1212
|
+
const resolvedScriptPath = await this.resolveV2FlowSkillScriptPath(
|
|
1213
|
+
customerIdn,
|
|
1214
|
+
projectIdn,
|
|
1215
|
+
agentIdn,
|
|
1216
|
+
flowIdn,
|
|
1217
|
+
skill.idn,
|
|
1218
|
+
runnerType,
|
|
1219
|
+
skill.prompt_script
|
|
1220
|
+
);
|
|
1221
|
+
|
|
1222
|
+
if (this.normalizePathForComparison(resolvedScriptPath) === normalizedScriptPath) {
|
|
1223
|
+
return {
|
|
1224
|
+
projectIdn,
|
|
1225
|
+
agentIdn,
|
|
1226
|
+
flowIdn,
|
|
1227
|
+
skillIdn: skill.idn,
|
|
1228
|
+
skillData: flowData.skills[skill.idn]
|
|
1229
|
+
};
|
|
1230
|
+
}
|
|
1231
|
+
}
|
|
1232
|
+
}
|
|
1233
|
+
}
|
|
1234
|
+
}
|
|
1235
|
+
|
|
1236
|
+
return null;
|
|
1237
|
+
}
|
|
1238
|
+
|
|
1239
|
+
private resolveV2SkillTargetFromCanonicalPath(
|
|
1240
|
+
scriptPath: string,
|
|
1241
|
+
mapData: ProjectMap
|
|
1242
|
+
): V2FlowSkillTarget | null {
|
|
1243
|
+
// Parse canonical V2 path:
|
|
1244
|
+
// .../newo_customers/{cust}/{proj}/agents/{agent}/flows/{flow}/skills/{skillFile}
|
|
1245
|
+
const pathParts = scriptPath.split('/');
|
|
1246
|
+
const skillFileName = pathParts[pathParts.length - 1] || '';
|
|
1247
|
+
const skillIdn = skillFileName.replace(/\.(nsl|nslg|jinja|guidance)$/, '');
|
|
1248
|
+
// skills/ -> flow/ -> flows/ -> agent/ -> agents/ -> project/
|
|
1249
|
+
const flowIdn = pathParts[pathParts.length - 3] || '';
|
|
1250
|
+
const agentIdn = pathParts[pathParts.length - 5] || '';
|
|
1251
|
+
const projectIdn = pathParts[pathParts.length - 7] || '';
|
|
1252
|
+
|
|
1253
|
+
const projectData = mapData.projects[projectIdn];
|
|
1254
|
+
const agentData = projectData?.agents[agentIdn];
|
|
1255
|
+
const flowData = agentData?.flows[flowIdn];
|
|
1256
|
+
|
|
1257
|
+
return {
|
|
1258
|
+
projectIdn,
|
|
1259
|
+
agentIdn,
|
|
1260
|
+
flowIdn,
|
|
1261
|
+
skillIdn,
|
|
1262
|
+
skillData: flowData?.skills[skillIdn]
|
|
1263
|
+
};
|
|
1264
|
+
}
|
|
1265
|
+
|
|
854
1266
|
/**
|
|
855
1267
|
* Push a V2 library skill update
|
|
856
1268
|
* Path: .../newo_customers/{cust}/{proj}/libraries/{lib}/skills/{skillFile}
|
|
@@ -949,11 +1361,30 @@ export class V2ProjectSyncStrategy implements ISyncStrategy<ProjectMeta, LocalPr
|
|
|
949
1361
|
}
|
|
950
1362
|
|
|
951
1363
|
for (const [flowIdn, flowData] of Object.entries(agentData.flows)) {
|
|
952
|
-
|
|
953
|
-
|
|
954
|
-
|
|
955
|
-
|
|
956
|
-
|
|
1364
|
+
const flowYamlSkills = await this.loadLocalV2FlowSkills(customer.idn, projectIdn, agentIdn, flowIdn);
|
|
1365
|
+
const skillIdns = new Set([
|
|
1366
|
+
...Object.keys(flowData.skills),
|
|
1367
|
+
...flowYamlSkills.keys()
|
|
1368
|
+
]);
|
|
1369
|
+
|
|
1370
|
+
for (const skillIdn of skillIdns) {
|
|
1371
|
+
const yamlSkill = flowYamlSkills.get(skillIdn);
|
|
1372
|
+
const skillMeta = flowData.skills[skillIdn];
|
|
1373
|
+
const runnerType = this.normalizeRunnerType(yamlSkill?.runner_type || skillMeta?.runner_type);
|
|
1374
|
+
const scriptPath = yamlSkill
|
|
1375
|
+
? await this.resolveV2FlowSkillScriptPath(
|
|
1376
|
+
customer.idn,
|
|
1377
|
+
projectIdn,
|
|
1378
|
+
agentIdn,
|
|
1379
|
+
flowIdn,
|
|
1380
|
+
skillIdn,
|
|
1381
|
+
runnerType,
|
|
1382
|
+
yamlSkill.prompt_script
|
|
1383
|
+
)
|
|
1384
|
+
: v2SkillScriptPath(
|
|
1385
|
+
customer.idn, projectIdn, agentIdn, flowIdn, skillIdn,
|
|
1386
|
+
runnerType
|
|
1387
|
+
);
|
|
957
1388
|
|
|
958
1389
|
if (await fs.pathExists(scriptPath)) {
|
|
959
1390
|
const content = await fs.readFile(scriptPath, 'utf8');
|
|
@@ -1002,6 +1433,25 @@ export class V2ProjectSyncStrategy implements ISyncStrategy<ProjectMeta, LocalPr
|
|
|
1002
1433
|
return changes;
|
|
1003
1434
|
}
|
|
1004
1435
|
|
|
1436
|
+
private async loadLocalV2FlowSkills(
|
|
1437
|
+
customerIdn: string,
|
|
1438
|
+
projectIdn: string,
|
|
1439
|
+
agentIdn: string,
|
|
1440
|
+
flowIdn: string
|
|
1441
|
+
): Promise<Map<string, V2InlineSkill>> {
|
|
1442
|
+
const flowYamlPath = v2FlowYamlPath(customerIdn, projectIdn, agentIdn, flowIdn);
|
|
1443
|
+
if (!(await fs.pathExists(flowYamlPath))) {
|
|
1444
|
+
return new Map();
|
|
1445
|
+
}
|
|
1446
|
+
|
|
1447
|
+
try {
|
|
1448
|
+
const flowDef = await parseV2FlowYaml(flowYamlPath);
|
|
1449
|
+
return new Map((flowDef.skills || []).map(skill => [skill.idn, skill]));
|
|
1450
|
+
} catch {
|
|
1451
|
+
return new Map();
|
|
1452
|
+
}
|
|
1453
|
+
}
|
|
1454
|
+
|
|
1005
1455
|
async validate(customer: CustomerConfig, _items: LocalProjectData[]): Promise<ValidationResult> {
|
|
1006
1456
|
const errors: ValidationError[] = [];
|
|
1007
1457
|
|
|
@@ -1020,7 +1470,61 @@ export class V2ProjectSyncStrategy implements ISyncStrategy<ProjectMeta, LocalPr
|
|
|
1020
1470
|
for (const [projectIdn, projectData] of Object.entries(mapData.projects)) {
|
|
1021
1471
|
for (const [agentIdn, agentData] of Object.entries(projectData.agents)) {
|
|
1022
1472
|
for (const [flowIdn, flowData] of Object.entries(agentData.flows)) {
|
|
1473
|
+
const flowYamlPath = v2FlowYamlPath(customer.idn, projectIdn, agentIdn, flowIdn);
|
|
1474
|
+
let localYamlSkills: Map<string, V2InlineSkill> | undefined;
|
|
1475
|
+
if (await fs.pathExists(flowYamlPath)) {
|
|
1476
|
+
try {
|
|
1477
|
+
const flowDef = await parseV2FlowYaml(flowYamlPath);
|
|
1478
|
+
localYamlSkills = new Map((flowDef.skills || []).map(s => [s.idn, s]));
|
|
1479
|
+
const skillIdns = new Set([
|
|
1480
|
+
...Object.keys(flowData.skills),
|
|
1481
|
+
...localYamlSkills.keys()
|
|
1482
|
+
]);
|
|
1483
|
+
|
|
1484
|
+
for (const skillIdn of skillIdns) {
|
|
1485
|
+
const localYamlSkill = localYamlSkills.get(skillIdn);
|
|
1486
|
+
const skillMeta = flowData.skills[skillIdn];
|
|
1487
|
+
|
|
1488
|
+
if (!localYamlSkill) {
|
|
1489
|
+
errors.push({
|
|
1490
|
+
field: `skill.${skillIdn}`,
|
|
1491
|
+
message: `Skill exists in project map but is missing from flow YAML: ${flowYamlPath}`,
|
|
1492
|
+
path: flowYamlPath
|
|
1493
|
+
});
|
|
1494
|
+
continue;
|
|
1495
|
+
}
|
|
1496
|
+
|
|
1497
|
+
const runnerType = this.normalizeRunnerType(
|
|
1498
|
+
localYamlSkill.runner_type || skillMeta?.runner_type
|
|
1499
|
+
);
|
|
1500
|
+
const scriptPath = await this.resolveV2FlowSkillScriptPath(
|
|
1501
|
+
customer.idn,
|
|
1502
|
+
projectIdn,
|
|
1503
|
+
agentIdn,
|
|
1504
|
+
flowIdn,
|
|
1505
|
+
skillIdn,
|
|
1506
|
+
runnerType,
|
|
1507
|
+
localYamlSkill.prompt_script
|
|
1508
|
+
);
|
|
1509
|
+
|
|
1510
|
+
if (!(await fs.pathExists(scriptPath))) {
|
|
1511
|
+
errors.push({
|
|
1512
|
+
field: `skill.${localYamlSkill.idn}`,
|
|
1513
|
+
message: `Script file not found: ${scriptPath}`,
|
|
1514
|
+
path: scriptPath
|
|
1515
|
+
});
|
|
1516
|
+
}
|
|
1517
|
+
}
|
|
1518
|
+
} catch {
|
|
1519
|
+
localYamlSkills = undefined;
|
|
1520
|
+
}
|
|
1521
|
+
}
|
|
1522
|
+
|
|
1023
1523
|
for (const [skillIdn, skillMeta] of Object.entries(flowData.skills)) {
|
|
1524
|
+
if (localYamlSkills) {
|
|
1525
|
+
continue;
|
|
1526
|
+
}
|
|
1527
|
+
|
|
1024
1528
|
const scriptPath = v2SkillScriptPath(
|
|
1025
1529
|
customer.idn, projectIdn, agentIdn, flowIdn, skillIdn,
|
|
1026
1530
|
skillMeta.runner_type
|