salesforce-metadata-mcp 2.5.2 → 2.5.3

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.
@@ -90,7 +90,7 @@ function getFreshTokenFromCLI(alias) {
90
90
  rawOutput = execSync(`sf org display --target-org ${alias} --json`, {
91
91
  encoding: "utf-8",
92
92
  timeout: 30_000,
93
- env: { PATH: process.env["PATH"] ?? "" },
93
+ env: { PATH: process.env["PATH"] ?? "", SF_TEMP_SHOW_SECRETS: "true" },
94
94
  });
95
95
  }
96
96
  catch (err) {
@@ -202,7 +202,10 @@ export function createClient(auth) {
202
202
  const text = await response.text().catch(() => "");
203
203
  throw new Error(sanitizeError(`Salesforce API error ${response.status}: ${text.slice(0, 300)}`));
204
204
  }
205
- const data = await response.json();
205
+ // 204 No Content has no body (e.g. PATCH record updates)
206
+ if (response.status === 204) return { data: {} };
207
+ const text = await response.text().catch(() => "");
208
+ const data = text ? JSON.parse(text) : {};
206
209
  return { data };
207
210
  }
208
211
  return {
@@ -265,7 +268,10 @@ export async function callMetadataSoap(auth, soapAction, bodyInner) {
265
268
  body: envelope,
266
269
  }, 60_000);
267
270
  if (!response.ok) {
268
- throw new Error(`SOAP request failed with HTTP ${response.status}.`);
271
+ const errText = await response.text().catch(() => "");
272
+ const faultMatch = errText.match(/<faultstring[^>]*>([\s\S]*?)<\/faultstring>/i);
273
+ const faultMsg = faultMatch ? faultMatch[1].trim() : "";
274
+ throw new Error(`SOAP request failed with HTTP ${response.status}${faultMsg ? `: ${faultMsg}` : "."}`);
269
275
  }
270
276
  return response.text();
271
277
  }
@@ -549,14 +555,38 @@ function buildFlowXml(params) {
549
555
  const hasFieldUpdates = params.fieldUpdates && params.fieldUpdates.length > 0;
550
556
  const hasApprovalSubmit = !!params.submitForApprovalProcessName;
551
557
  const hasElements = params.elements && params.elements.length > 0;
558
+ // When no elements provided add a no-op assignment so the flow can be activated
559
+ const needsNoOp = !hasElements && !hasApprovalSubmit && !hasFieldUpdates;
552
560
  const firstTarget = hasElements && params.elements[0]
553
561
  ? params.elements[0].name
554
562
  : hasApprovalSubmit ? "Submit_For_Approval"
555
563
  : hasFieldUpdates ? "Update_Record"
564
+ : needsNoOp ? "No_Op"
556
565
  : null;
557
566
  const startConnector = firstTarget
558
567
  ? `<met:connector><met:targetReference>${firstTarget}</met:targetReference></met:connector>` : "";
559
- const startElement = params.flowType === "RecordTriggeredFlow" && params.triggerObject ? `
568
+ const noOpElement = needsNoOp ? `
569
+ <met:assignments>
570
+ <met:name>No_Op</met:name>
571
+ <met:label>No Op</met:label>
572
+ <met:locationX>250</met:locationX>
573
+ <met:locationY>100</met:locationY>
574
+ <met:assignmentItems>
575
+ <met:assignToReference>NoOpVar</met:assignToReference>
576
+ <met:operator>Assign</met:operator>
577
+ <met:value><met:stringValue></met:stringValue></met:value>
578
+ </met:assignmentItems>
579
+ </met:assignments>` : "";
580
+ const noOpVar = needsNoOp ? `
581
+ <met:variables>
582
+ <met:name>NoOpVar</met:name>
583
+ <met:dataType>String</met:dataType>
584
+ <met:isCollection>false</met:isCollection>
585
+ <met:isInput>false</met:isInput>
586
+ <met:isOutput>false</met:isOutput>
587
+ </met:variables>` : "";
588
+ const isRecordTriggered = params.flowType === "RecordTriggeredFlow" && params.triggerObject;
589
+ const startElement = isRecordTriggered ? `
560
590
  <met:start>
561
591
  <met:locationX>50</met:locationX><met:locationY>0</met:locationY>
562
592
  <met:object>${x(params.triggerObject)}</met:object>
@@ -605,28 +635,33 @@ function buildFlowXml(params) {
605
635
  </met:value>
606
636
  </met:inputAssignments>`).join("\n")}
607
637
  </met:recordUpdates>` : "";
638
+ const processType = params.flowType === "Screen" ? "Flow"
639
+ : (params.flowType === "RecordTriggeredFlow" || params.flowType === "AutoLaunchedFlow") ? "AutoLaunchedFlow"
640
+ : x(params.flowType);
608
641
  return `<met:metadata xsi:type="met:Flow" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance">
609
- <met:fullName>${x(params.apiName)}</met:fullName>
642
+ <met:fullName>${x(params.apiName ?? params.flowName)}</met:fullName>
610
643
  <met:label>${x(params.label)}</met:label>
611
644
  <met:apiVersion>${API_VERSION}</met:apiVersion>
612
- <met:status>${x(params.status)}</met:status>
613
- <met:processType>${params.flowType === "Screen" ? "Flow" : params.flowType === "RecordTriggeredFlow" ? "AutoLaunchedFlow" : x(params.flowType)}</met:processType>
614
- <met:environments>Default</met:environments>
645
+ <met:status>${x(params.status ?? "Draft")}</met:status>
646
+ <met:processType>${processType}</met:processType>
647
+ ${params.runInMode ? `<met:runInMode>${x(params.runInMode)}</met:runInMode>` : ""}
615
648
  ${params.description ? `<met:description>${x(params.description)}</met:description>` : ""}
649
+ ${noOpVar}
616
650
  ${vars}
617
651
  ${startElement}
618
652
  ${buildElements(params.elements)}
653
+ ${noOpElement}
619
654
  ${approvalSubmitElement}
620
655
  ${recordUpdateElement}
621
656
  </met:metadata>`;
622
657
  }
623
658
  function buildApprovalProcessXml(params) {
624
- const submitters = params.allowedSubmitters.map(s => `
659
+ const submitters = (params.allowedSubmitters ?? [{ type: "owner" }]).map(s => `
625
660
  <met:allowedSubmitters>
626
661
  <met:type>${x(s.type)}</met:type>
627
662
  ${s.submitter ? `<met:submitter>${x(s.submitter)}</met:submitter>` : ""}
628
663
  </met:allowedSubmitters>`).join("\n");
629
- const steps = params.approvalSteps.map((step, i) => `
664
+ const steps = (params.approvalSteps ?? []).map((step, i) => `
630
665
  <met:approvalStep>
631
666
  <met:name>${x(step.name)}</met:name>
632
667
  <met:label>${x(step.label)}</met:label>
@@ -666,26 +701,30 @@ function buildApprovalProcessXml(params) {
666
701
  ${steps}
667
702
  ${entryCriteriaXml}
668
703
  ${params.emailTemplate ? `<met:emailTemplate>${x(params.emailTemplate)}</met:emailTemplate>` : ""}
669
- <met:finalApprovalRecordLock>${params.finalApprovalLock}</met:finalApprovalRecordLock>
670
- <met:finalRejectionRecordLock>${params.finalRejectionLock}</met:finalRejectionRecordLock>
704
+ <met:finalApprovalRecordLock>${params.finalApprovalLock ?? false}</met:finalApprovalRecordLock>
705
+ <met:finalRejectionRecordLock>${params.finalRejectionLock ?? false}</met:finalRejectionRecordLock>
671
706
  <met:recordEditability>${x(params.recordEditability)}</met:recordEditability>
672
707
  <met:showApprovalHistory>true</met:showApprovalHistory>
673
708
  </met:metadata>`;
674
709
  }
675
710
  function buildValidationRuleXml(params) {
711
+ const objectName = params.objectName ?? params.objectApiName;
712
+ // WSDL order for ValidationRule: fullName (base), active, description, errorConditionFormula, errorDisplayField, errorMessage
676
713
  return `<met:metadata xsi:type="met:CustomObject" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance">
677
- <met:fullName>${x(params.objectName)}</met:fullName>
714
+ <met:fullName>${x(objectName)}</met:fullName>
678
715
  <met:validationRules>
679
716
  <met:fullName>${x(params.ruleName)}</met:fullName>
680
- <met:active>${params.active}</met:active>
717
+ <met:active>${params.active !== false ? "true" : "false"}</met:active>
718
+ ${params.description ? `<met:description>${x(params.description)}</met:description>` : ""}
681
719
  <met:errorConditionFormula>${x(params.errorConditionFormula)}</met:errorConditionFormula>
682
- <met:errorMessage>${x(params.errorMessage)}</met:errorMessage>
683
720
  ${params.errorDisplayField ? `<met:errorDisplayField>${x(params.errorDisplayField)}</met:errorDisplayField>` : ""}
684
- ${params.description ? `<met:description>${x(params.description)}</met:description>` : ""}
721
+ <met:errorMessage>${x(params.errorMessage)}</met:errorMessage>
685
722
  </met:validationRules>
686
723
  </met:metadata>`;
687
724
  }
688
725
  function buildWorkflowFieldUpdateXml(params) {
726
+ const actionName = params.actionName ?? params.updateName ?? "FieldUpdate";
727
+ const fieldName = params.field ?? params.fieldName ?? "";
689
728
  let operationXml = "";
690
729
  if (params.nullValue) {
691
730
  operationXml = `<met:operation>Null</met:operation>`;
@@ -696,16 +735,14 @@ function buildWorkflowFieldUpdateXml(params) {
696
735
  else {
697
736
  operationXml = `<met:literalValue>${x(params.literalValue ?? "")}</met:literalValue><met:operation>Literal</met:operation>`;
698
737
  }
699
- return `<met:metadata xsi:type="met:Workflow" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance">
700
- <met:fullName>${x(params.objectName)}</met:fullName>
701
- <met:fieldUpdates>
702
- <met:name>${x(params.actionName)}</met:name>
703
- <met:label>${x(params.label)}</met:label>
704
- <met:field>${x(params.field)}</met:field>
705
- ${operationXml}
706
- <met:notifyAssignee>${params.notifyAssignee}</met:notifyAssignee>
707
- <met:protected>false</met:protected>
708
- </met:fieldUpdates>
738
+ return `<met:metadata xsi:type="met:WorkflowFieldUpdate" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance">
739
+ <met:fullName>${x(params.objectName)}.${x(actionName)}</met:fullName>
740
+ <met:name>${x(actionName)}</met:name>
741
+ <met:label>${x(params.label ?? actionName)}</met:label>
742
+ <met:field>${x(fieldName)}</met:field>
743
+ ${operationXml}
744
+ <met:notifyAssignee>${params.notifyAssignee ?? false}</met:notifyAssignee>
745
+ <met:protected>false</met:protected>
709
746
  </met:metadata>`;
710
747
  }
711
748
  // ─── New XML builders for expanded tools ─────────────────────────────────────
@@ -716,7 +753,6 @@ function buildCustomMetadataTypeXml(params) {
716
753
  <met:label>${x(params.label)}</met:label>
717
754
  <met:pluralLabel>${x(params.pluralLabel)}</met:pluralLabel>
718
755
  ${params.description ? `<met:description>${x(params.description)}</met:description>` : ""}
719
- <met:deploymentStatus>Deployed</met:deploymentStatus>
720
756
  ${fieldsXml}
721
757
  </met:metadata>`;
722
758
  }
@@ -750,9 +786,7 @@ function buildCustomSettingXml(params) {
750
786
  return `<met:metadata xsi:type="met:CustomObject" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance">
751
787
  <met:fullName>${x(params.fullName)}</met:fullName>
752
788
  <met:label>${x(params.label)}</met:label>
753
- <met:customSettingsType>${x(params.settingType)}</met:customSettingsType>
754
- <met:customSettingsVisibility>${x(params.visibility)}</met:customSettingsVisibility>
755
- <met:deploymentStatus>Deployed</met:deploymentStatus>
789
+ <met:customSettingsType>${x(params.settingType ?? "List")}</met:customSettingsType>
756
790
  ${params.description ? `<met:description>${x(params.description)}</met:description>` : ""}
757
791
  ${fieldsXml}
758
792
  </met:metadata>`;
@@ -786,36 +820,37 @@ function buildPlatformEventXml(params) {
786
820
  </met:metadata>`;
787
821
  }
788
822
  function buildEmailAlertXml(params) {
789
- const recipientsXml = params.recipients.map(r => `
823
+ const recipientsXml = (params.recipients ?? []).map(r => `
790
824
  <met:recipients>
791
825
  <met:type>${x(r.type)}</met:type>
792
826
  ${r.recipient ? `<met:recipient>${x(r.recipient)}</met:recipient>` : ""}
793
827
  </met:recipients>`).join("\n");
794
- return `<met:metadata xsi:type="met:Workflow" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance">
795
- <met:fullName>${x(params.objectName)}</met:fullName>
796
- <met:alerts>
797
- <met:fullName>${x(params.alertName)}</met:fullName>
798
- <met:label>${x(params.label)}</met:label>
799
- <met:template>${x(params.template)}</met:template>
800
- <met:senderType>${x(params.senderType)}</met:senderType>
801
- ${params.senderAddress ? `<met:senderAddress>${x(params.senderAddress)}</met:senderAddress>` : ""}
802
- ${params.description ? `<met:description>${x(params.description)}</met:description>` : ""}
803
- <met:protected>${params.protected}</met:protected>
804
- ${recipientsXml}
805
- </met:alerts>
828
+ const alertName = params.alertName ?? params.label ?? "EmailAlert";
829
+ return `<met:metadata xsi:type="met:WorkflowAlert" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance">
830
+ <met:fullName>${x(params.objectName)}.${x(alertName)}</met:fullName>
831
+ <met:description>${x(params.description ?? params.label ?? alertName)}</met:description>
832
+ <met:protected>${params.protected ?? false}</met:protected>
833
+ <met:senderType>${x(params.senderType ?? "CurrentUser")}</met:senderType>
834
+ ${params.senderAddress ? `<met:senderAddress>${x(params.senderAddress)}</met:senderAddress>` : ""}
835
+ <met:template>${x(params.template)}</met:template>
836
+ ${recipientsXml}
806
837
  </met:metadata>`;
807
838
  }
808
839
  function buildAssignmentRuleXml(params) {
809
- const entriesXml = params.ruleEntries.map(e => {
810
- const criteriaXml = (e.criteriaItems ?? []).map(c => `
840
+ const entriesXml = params.ruleEntries.map((e, idx) => {
841
+ const criteriaXml = (e.criteriaItems ?? []).map(c => {
842
+ const field = c.field.includes(".") ? c.field : `${params.objectName}.${c.field}`;
843
+ return `
811
844
  <met:criteriaItems>
812
- <met:field>${x(c.field)}</met:field>
845
+ <met:field>${x(field)}</met:field>
813
846
  <met:operation>${x(c.operation)}</met:operation>
814
847
  <met:value>${x(c.value)}</met:value>
815
- </met:criteriaItems>`).join("\n");
848
+ </met:criteriaItems>`;
849
+ }).join("\n");
816
850
  return `<met:ruleEntry>
851
+ <met:entryOrder>${e.entryOrder ?? idx + 1}</met:entryOrder>
817
852
  <met:assignedTo>${x(e.assignedTo)}</met:assignedTo>
818
- <met:assignedToType>${x(e.assignedToType)}</met:assignedToType>
853
+ <met:assignedToType>${x(e.assignedToType ?? "Queue")}</met:assignedToType>
819
854
  ${criteriaXml}
820
855
  ${e.formula ? `<met:formula>${x(e.formula)}</met:formula>` : ""}
821
856
  ${e.booleanFilter ? `<met:booleanFilter>${x(e.booleanFilter)}</met:booleanFilter>` : ""}
@@ -826,7 +861,7 @@ function buildAssignmentRuleXml(params) {
826
861
  <met:fullName>${x(params.objectName)}</met:fullName>
827
862
  <met:assignmentRule>
828
863
  <met:fullName>${x(params.ruleName)}</met:fullName>
829
- <met:active>${params.active}</met:active>
864
+ <met:active>${params.active ?? false}</met:active>
830
865
  ${entriesXml}
831
866
  </met:assignmentRule>
832
867
  </met:metadata>`;
@@ -919,13 +954,19 @@ function buildNamedCredentialXml(params) {
919
954
  </met:metadata>`;
920
955
  }
921
956
  function buildLightningAppXml(params) {
922
- const navItemsXml = (params.navItems ?? []).map(ni => `
957
+ const navItemsXml = (params.navItems ?? []).map(ni => {
958
+ const name = typeof ni === "string" ? ni : ni.name;
959
+ const type = typeof ni === "string" ? "StandardNavItem" : (ni.type ?? "StandardNavItem");
960
+ const label = typeof ni === "string" ? null : ni.label;
961
+ const defaultItem = typeof ni === "string" ? false : (ni.defaultItem ?? false);
962
+ return `
923
963
  <met:navItems>
924
- <met:name>${x(ni.name)}</met:name>
925
- <met:type>${x(ni.type)}</met:type>
926
- ${ni.label ? `<met:label>${x(ni.label)}</met:label>` : ""}
927
- <met:defaultItem>${ni.defaultItem}</met:defaultItem>
928
- </met:navItems>`).join("\n");
964
+ <met:name>${x(name)}</met:name>
965
+ <met:type>${x(type)}</met:type>
966
+ ${label ? `<met:label>${x(label)}</met:label>` : ""}
967
+ <met:defaultItem>${defaultItem}</met:defaultItem>
968
+ </met:navItems>`;
969
+ }).join("\n");
929
970
  const utilityXml = (params.utilityItems ?? []).map(u => `
930
971
  <met:utilityBar>
931
972
  <met:name>${x(u.name)}</met:name>
@@ -947,15 +988,19 @@ function buildLightningAppXml(params) {
947
988
  </met:metadata>`;
948
989
  }
949
990
  function buildTabXml(params) {
991
+ const sobjectName = params.sobjectName;
992
+ const isCustomObject = params.customObject ?? (sobjectName ? true : !params.url && !params.page);
993
+ // For custom SObject tabs: fullName IS the sobjectName (e.g. MCPTest__c); sobjectName field is not used in SOAP
994
+ const fullName = (isCustomObject && sobjectName) ? sobjectName : params.fullName;
995
+ // WSDL element order: customObject, description, fullName, label, motif, page, url
950
996
  return `<met:metadata xsi:type="met:CustomTab" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance">
951
- <met:fullName>${x(params.fullName)}</met:fullName>
952
- <met:motif>${x(params.motif)}</met:motif>
953
- <met:customObject>${params.customObject}</met:customObject>
954
- ${params.sobjectName ? `<met:sobjectName>${x(params.sobjectName)}</met:sobjectName>` : ""}
997
+ <met:customObject>${isCustomObject}</met:customObject>
998
+ ${params.description ? `<met:description>${x(params.description)}</met:description>` : ""}
999
+ <met:fullName>${x(fullName)}</met:fullName>
955
1000
  ${params.label ? `<met:label>${x(params.label)}</met:label>` : ""}
956
- ${params.url ? `<met:url>${x(params.url)}</met:url>` : ""}
1001
+ ${params.motif ? `<met:motif>${x(params.motif)}</met:motif>` : ""}
957
1002
  ${params.page ? `<met:page>${x(params.page)}</met:page>` : ""}
958
- ${params.description ? `<met:description>${x(params.description)}</met:description>` : ""}
1003
+ ${params.url ? `<met:url>${x(params.url)}</met:url>` : ""}
959
1004
  </met:metadata>`;
960
1005
  }
961
1006
  function buildCompactLayoutXml(params) {
@@ -1029,6 +1074,15 @@ function buildReportTypeXml(params) {
1029
1074
  </met:columns>
1030
1075
  <met:masterLabel>${x(r.label ?? r.joinTable)}</met:masterLabel>
1031
1076
  </met:sections>`).join("\n");
1077
+ // Base sections element — include Name plus any additional columns specified
1078
+ const baseFields = ["Name", ...(params.columns ?? []).filter(c => c !== "Name")];
1079
+ const baseColsXml = baseFields.map(f => `<met:columns><met:field>${x(f)}</met:field><met:table>${x(params.baseObject)}</met:table></met:columns>`).join("\n ");
1080
+ const baseSectionsXml = `
1081
+ <met:sections>
1082
+ ${baseColsXml}
1083
+ <met:masterLabel>${x(params.label ?? params.baseObject)}</met:masterLabel>
1084
+ </met:sections>`;
1085
+
1032
1086
  const joinXml = (params.relationships ?? []).map((r, i) => `
1033
1087
  <met:relationships>
1034
1088
  <met:join>
@@ -1037,29 +1091,43 @@ function buildReportTypeXml(params) {
1037
1091
  </met:join>
1038
1092
  <met:preferredRelationship>${x(r.joinTable)}</met:preferredRelationship>
1039
1093
  </met:relationships>`).join("\n");
1094
+ // WSDL element order: baseObject, category, deployed, description, fullName, label, relationships, sections
1040
1095
  return `<met:metadata xsi:type="met:ReportType" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance">
1041
- <met:fullName>${x(params.fullName)}</met:fullName>
1042
- <met:label>${x(params.label)}</met:label>
1043
1096
  <met:baseObject>${x(params.baseObject)}</met:baseObject>
1044
1097
  <met:category>${x(params.category)}</met:category>
1045
1098
  <met:deployed>${params.deployed}</met:deployed>
1046
1099
  ${params.description ? `<met:description>${x(params.description)}</met:description>` : ""}
1047
- ${relXml}
1100
+ <met:fullName>${x(params.fullName)}</met:fullName>
1101
+ <met:label>${x(params.label)}</met:label>
1048
1102
  ${joinXml}
1103
+ ${baseSectionsXml}
1104
+ ${relXml}
1049
1105
  </met:metadata>`;
1050
1106
  }
1051
1107
  function buildConnectedAppXml(params) {
1052
- const callbacksXml = params.callbackUrls.map(u => `<met:callbackUrl>${x(u)}</met:callbackUrl>`).join("\n");
1053
- const scopesXml = params.scopes.map(s => `<met:scopes>${x(s)}</met:scopes>`).join("\n");
1108
+ // ConnectedAppOauthAccessScope enum uses Title case in Salesforce Metadata API: Api, Web, Basic, Full, etc.
1109
+ const scopeMap = {
1110
+ api: "Api", web: "Web", basic: "Basic", full: "Full", chatter: "Chatter",
1111
+ openid: "OpenID", openId: "OpenID", refresh_token: "RefreshToken",
1112
+ offline_access: "OfflineAccess", customPermissions: "CustomPermissions",
1113
+ // already correct Title case → passthrough
1114
+ Api: "Api", Web: "Web", Basic: "Basic", Full: "Full", Chatter: "Chatter",
1115
+ OpenID: "OpenID", RefreshToken: "RefreshToken", OfflineAccess: "OfflineAccess",
1116
+ CustomPermissions: "CustomPermissions",
1117
+ };
1118
+ const callbacksXml = (params.callbackUrls ?? []).map(u => `<met:callbackUrl>${x(u)}</met:callbackUrl>`).join("\n");
1119
+ const scopesXml = (params.scopes ?? []).map(s => `<met:scopes>${x(scopeMap[s] ?? s)}</met:scopes>`).join("\n");
1120
+ // WSDL order for ConnectedApp: fullName (base), contactEmail, description, label, oauthConfig
1121
+ // WSDL order for ConnectedAppOauthConfig: callbackUrl, consumerKey, scopes, startUrl
1054
1122
  return `<met:metadata xsi:type="met:ConnectedApp" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance">
1055
1123
  <met:fullName>${x(params.fullName)}</met:fullName>
1056
- <met:label>${x(params.label)}</met:label>
1057
- ${params.description ? `<met:description>${x(params.description)}</met:description>` : ""}
1058
1124
  <met:contactEmail>${x(params.contactEmail)}</met:contactEmail>
1125
+ ${params.description ? `<met:description>${x(params.description)}</met:description>` : ""}
1126
+ <met:label>${x(params.label)}</met:label>
1059
1127
  <met:oauthConfig>
1060
1128
  ${callbacksXml}
1061
- ${scopesXml}
1062
1129
  ${params.consumerKey ? `<met:consumerKey>${x(params.consumerKey)}</met:consumerKey>` : ""}
1130
+ ${scopesXml}
1063
1131
  ${params.startUrl ? `<met:startUrl>${x(params.startUrl)}</met:startUrl>` : ""}
1064
1132
  </met:oauthConfig>
1065
1133
  </met:metadata>`;
@@ -1099,14 +1167,15 @@ function buildRemoteSiteSettingXml(params) {
1099
1167
  </met:metadata>`;
1100
1168
  }
1101
1169
  function buildCspTrustedSiteXml(params) {
1102
- const name = params.endpointUrl.replace(/[^A-Za-z0-9]/g, "_").slice(0, 40);
1103
- const directivesXml = params.cspDirectives.map(d => `<met:cspDirectives>${x(d)}</met:cspDirectives>`).join("\n");
1170
+ // Strip protocol and generate a valid API name (no double-underscores, must not look like a namespace)
1171
+ const rawName = params.endpointUrl.replace(/^https?:\/\//, "").replace(/[^A-Za-z0-9]/g, "_").replace(/_+/g, "_").replace(/^_|_$/g, "");
1172
+ const name = params.siteName ?? ("Csp_" + rawName).slice(0, 40);
1173
+ // WSDL order: fullName (base), context, description, endpointUrl, isActive, isApplicableToConnectSrc, isApplicableToFontSrc, isApplicableToFrameSrc, isApplicableToImgSrc, isApplicableToMediaSrc, isApplicableToScriptSrc, isApplicableToStyleSrc
1104
1174
  return `<met:metadata xsi:type="met:CspTrustedSite" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance">
1105
1175
  <met:fullName>${x(name)}</met:fullName>
1106
- <met:endpointUrl>${x(params.endpointUrl)}</met:endpointUrl>
1107
1176
  ${params.description ? `<met:description>${x(params.description)}</met:description>` : ""}
1108
- <met:isActive>${params.isActive}</met:isActive>
1109
- ${directivesXml}
1177
+ <met:endpointUrl>${x(params.endpointUrl)}</met:endpointUrl>
1178
+ <met:isActive>${params.isActive !== false ? "true" : "false"}</met:isActive>
1110
1179
  </met:metadata>`;
1111
1180
  }
1112
1181
  function buildSharingRuleXml(params) {
@@ -1116,9 +1185,19 @@ function buildSharingRuleXml(params) {
1116
1185
  <met:operation>${x(c.operation)}</met:operation>
1117
1186
  <met:value>${x(c.value)}</met:value>
1118
1187
  </met:criteriaItems>`).join("\n");
1119
- const sharedToXml = `<met:sharedTo><met:${x(params.sharedTo.type)}>${params.sharedTo.name ? x(params.sharedTo.name) : ""}</met:${x(params.sharedTo.type)}></met:sharedTo>`;
1188
+ const sharingTargetXml = (target) => {
1189
+ if (!target) return "";
1190
+ if (typeof target === "string") {
1191
+ const lower = target.toLowerCase();
1192
+ if (lower === "allinternalusers") return `<met:allInternalUsers>true</met:allInternalUsers>`;
1193
+ if (lower === "allcustomerportaluser" || lower === "allcustomerportalusers") return `<met:allCustomerPortalUsers>true</met:allCustomerPortalUsers>`;
1194
+ return `<met:role>${x(target)}</met:role>`;
1195
+ }
1196
+ return `<met:${x(target.type)}>${target.name ? x(target.name) : ""}</met:${x(target.type)}>`;
1197
+ };
1198
+ const sharedToXml = `<met:sharedTo>${sharingTargetXml(params.sharedTo)}</met:sharedTo>`;
1120
1199
  const sharedFromXml = params.sharedFrom
1121
- ? `<met:sharedFrom><met:${x(params.sharedFrom.type)}>${params.sharedFrom.name ? x(params.sharedFrom.name) : ""}</met:${x(params.sharedFrom.type)}></met:sharedFrom>` : "";
1200
+ ? `<met:sharedFrom>${sharingTargetXml(params.sharedFrom)}</met:sharedFrom>` : "";
1122
1201
  return `<met:metadata xsi:type="met:SharingRules" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance">
1123
1202
  <met:fullName>${x(params.objectName)}</met:fullName>
1124
1203
  <met:${params.ruleType === "criteria" ? "sharingCriteriaRules" : "sharingOwnerRules"}>
@@ -1150,12 +1229,16 @@ function buildRecordTypeXml(params) {
1150
1229
  </met:metadata>`;
1151
1230
  }
1152
1231
  function buildBusinessProcessXml(params) {
1153
- const valuesXml = params.values.map(v => `
1232
+ const valuesXml = (params.values ?? []).map(v => {
1233
+ const name = typeof v === "string" ? v : (v.fullName ?? v.name ?? v.value ?? "");
1234
+ const isDefault = typeof v === "string" ? false : (v.default ?? false);
1235
+ return `
1154
1236
  <met:values>
1155
- <met:fullName>${x(v)}</met:fullName>
1156
- <met:default>false</met:default>
1237
+ <met:fullName>${x(name)}</met:fullName>
1238
+ <met:default>${isDefault}</met:default>
1157
1239
  <met:closed>false</met:closed>
1158
- </met:values>`).join("\n");
1240
+ </met:values>`;
1241
+ }).join("\n");
1159
1242
  return `<met:metadata xsi:type="met:BusinessProcess" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance">
1160
1243
  <met:fullName>${x(params.objectName)}.${x(params.processName)}</met:fullName>
1161
1244
  <met:isActive>${params.isActive}</met:isActive>
@@ -1191,62 +1274,78 @@ function buildPageLayoutXml(params) {
1191
1274
  ${relatedListsXml}
1192
1275
  </met:metadata>`;
1193
1276
  }
1194
- function buildFieldDependencyXml(params) {
1195
- const settingsXml = params.valueSettings.map(vs => `
1196
- <met:valueSettings>
1197
- ${vs.controllingFieldValue.map(cfv => `<met:controllingFieldValue>${x(cfv)}</met:controllingFieldValue>`).join("\n")}
1198
- <met:valueName>${x(vs.valueName)}</met:valueName>
1199
- </met:valueSettings>`).join("\n");
1277
+ function buildFieldDependencyXml(params, fieldLabel, fieldType, existingValues = []) {
1278
+ const settingsXml = (params.valueSettings ?? []).flatMap(vs => {
1279
+ const ctrlVals = Array.isArray(vs.controllingFieldValue) ? vs.controllingFieldValue : [vs.controllingFieldValue];
1280
+ const depVals = vs.dependentValues ?? (vs.valueName ? [vs.valueName] : []);
1281
+ return depVals.map(dep => `
1282
+ <met:valueSettings>
1283
+ ${ctrlVals.map(cfv => `<met:controllingFieldValue>${x(cfv)}</met:controllingFieldValue>`).join("\n")}
1284
+ <met:valueName>${x(dep)}</met:valueName>
1285
+ </met:valueSettings>`);
1286
+ }).join("\n");
1287
+ // Include existing picklist values in valueSetDefinition — required to avoid "specify picklist/valueSet" error
1288
+ const valuesXml = existingValues.map(v => `
1289
+ <met:value>
1290
+ <met:fullName>${x(v.fullName)}</met:fullName>
1291
+ <met:default>${v.isDefault ? "true" : "false"}</met:default>
1292
+ <met:label>${x(v.label)}</met:label>
1293
+ </met:value>`).join("\n");
1294
+ const valueSetDefXml = valuesXml ? `
1295
+ <met:valueSetDefinition>
1296
+ <met:sorted>false</met:sorted>
1297
+ ${valuesXml}
1298
+ </met:valueSetDefinition>` : "";
1200
1299
  return `<met:metadata xsi:type="met:CustomField" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance">
1201
1300
  <met:fullName>${x(params.objectName)}.${x(params.dependentField)}</met:fullName>
1202
- <met:fieldDependency>
1301
+ <met:label>${x(fieldLabel)}</met:label>
1302
+ <met:type>${x(fieldType)}</met:type>
1303
+ <met:valueSet>
1203
1304
  <met:controllingField>${x(params.controllingField)}</met:controllingField>
1305
+ ${valueSetDefXml}
1204
1306
  ${settingsXml}
1205
- </met:fieldDependency>
1307
+ </met:valueSet>
1206
1308
  </met:metadata>`;
1207
1309
  }
1208
1310
  function buildNetworkXml(params) {
1311
+ // Minimal Network XML — many fields (loginType, picassoSite, template) are absent from v66 WSDL
1312
+ // NOTE: 'label' is NOT a valid Network field; status is the key required field
1209
1313
  return `<met:metadata xsi:type="met:Network" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance">
1210
1314
  <met:fullName>${x(params.siteName)}</met:fullName>
1211
- <met:label>${x(params.label)}</met:label>
1212
- <met:urlPathPrefix>${x(params.urlPathPrefix)}</met:urlPathPrefix>
1213
- <met:template><met:name>${x(params.template)}</met:name></met:template>
1214
- <met:status>${x(params.status)}</met:status>
1215
1315
  ${params.description ? `<met:description>${x(params.description)}</met:description>` : ""}
1216
- ${params.guestUserProfile ? `<met:guestProfile>${x(params.guestUserProfile)}</met:guestProfile>` : ""}
1217
- <met:allowedExtensions>jpg,jpeg,png,gif,pdf,doc,docx</met:allowedExtensions>
1218
- <met:enableGuestChatter>false</met:enableGuestChatter>
1219
- <met:enableInvitation>false</met:enableInvitation>
1220
- <met:enableKnowledgeable>false</met:enableKnowledgeable>
1221
- <met:enableNicknameDisplay>false</met:enableNicknameDisplay>
1222
- <met:enablePrivateMessages>false</met:enablePrivateMessages>
1223
- <met:enableReputation>false</met:enableReputation>
1224
- <met:enableShowAllNetworkSettings>false</met:enableShowAllNetworkSettings>
1225
- <met:enableTalkingAboutStats>false</met:enableTalkingAboutStats>
1226
- <met:gatherCustomerSentimentData>false</met:gatherCustomerSentimentData>
1227
- <met:loginType>CommunitiesLogin</met:loginType>
1228
- <met:networkMemberGroups></met:networkMemberGroups>
1229
- <met:picassoSite>${x(params.siteName)}</met:picassoSite>
1230
- <met:selfRegistration>false</met:selfRegistration>
1231
- <met:sendWelcomeEmail>false</met:sendWelcomeEmail>
1232
- <met:tabs></met:tabs>
1233
- <met:verificationRequired>Relaxed</met:verificationRequired>
1316
+ <met:status>${x(params.status ?? "UnderConstruction")}</met:status>
1317
+ <met:urlPathPrefix>${x(params.urlPathPrefix ?? params.siteName.toLowerCase().replace(/[^a-z0-9]/g, ""))}</met:urlPathPrefix>
1234
1318
  </met:metadata>`;
1235
1319
  }
1236
1320
  function buildBotXml(params) {
1321
+ // BotType enum values: InternalCopilot, ExternalCopilot, Bot.
1322
+ // EinsteinCopilot/EinsteinServiceAgent are user-facing aliases that map to InternalCopilot.
1323
+ const rawType = params.type ?? params.agentType ?? "InternalCopilot";
1324
+ const botType = (rawType === "EinsteinServiceAgent" || rawType === "EinsteinCopilot") ? "InternalCopilot" : rawType;
1325
+ const agentType = botType === "ExternalCopilot" ? "EinsteinServiceAgent" : "EinsteinServiceAgent";
1326
+ // WSDL alphabetical order: agentTemplate, agentType, botSource, botVersions, description, fullName, label, logPrivateConversationData, richContentEnabled, sessionTimeout, type
1327
+ // botVersions with entryDialog+botDialogs is required on create; omit on update (causes "Invalid entry dialog" error)
1328
+ const botVersionsXml = params.isNew !== false ? `<met:botVersions>
1329
+ <met:botDialogs>
1330
+ <met:developerName>Welcome</met:developerName>
1331
+ <met:label>Welcome</met:label>
1332
+ <met:showInFooterMenu>false</met:showInFooterMenu>
1333
+ </met:botDialogs>
1334
+ <met:entryDialog>Welcome</met:entryDialog>
1335
+ <met:fullName>v1</met:fullName>
1336
+ </met:botVersions>` : "";
1237
1337
  return `<met:metadata xsi:type="met:Bot" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance">
1238
- <met:fullName>${x(params.agentName)}</met:fullName>
1239
- <met:label>${x(params.label)}</met:label>
1240
- ${params.description ? `<met:description>${x(params.description)}</met:description>` : ""}
1241
1338
  <met:agentTemplate>AiCopilot__AgentforceAgent</met:agentTemplate>
1242
- <met:agentType>EinsteinServiceAgent</met:agentType>
1339
+ <met:agentType>${x(agentType)}</met:agentType>
1243
1340
  <met:botSource>None</met:botSource>
1341
+ ${botVersionsXml}
1342
+ ${params.description ? `<met:description>${x(params.description)}</met:description>` : ""}
1343
+ <met:fullName>${x(params.agentName)}</met:fullName>
1344
+ <met:label>${x(params.label)}</met:label>
1244
1345
  <met:logPrivateConversationData>false</met:logPrivateConversationData>
1245
1346
  <met:richContentEnabled>true</met:richContentEnabled>
1246
1347
  <met:sessionTimeout>0</met:sessionTimeout>
1247
- <met:type>${x(params.type ?? "EinsteinCopilot")}</met:type>
1248
- ${params.systemPrompt ? `<met:systemPrompt>${x(params.systemPrompt)}</met:systemPrompt>` : ""}
1249
- ${params.openingMessage ? `<met:welcomeMessage>${x(params.openingMessage)}</met:welcomeMessage>` : ""}
1348
+ <met:type>${x(botType)}</met:type>
1250
1349
  </met:metadata>`;
1251
1350
  }
1252
1351
  function buildGenAiPluginXml(params) {
@@ -1261,14 +1360,16 @@ function buildGenAiPluginXml(params) {
1261
1360
  <met:genAiFunctions>
1262
1361
  <met:functionName>${x(a)}</met:functionName>
1263
1362
  </met:genAiFunctions>`).join("\n");
1363
+ const topicName = params.topicName ?? params.fullName ?? params.label ?? "";
1264
1364
  return `<met:metadata xsi:type="met:GenAiPlugin" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance">
1265
- <met:fullName>${x(params.topicName)}</met:fullName>
1266
- <met:masterLabel>${x(params.label)}</met:masterLabel>
1267
- <met:description>${x(params.description)}</met:description>
1268
- <met:scope>${x(params.scope)}</met:scope>
1269
- <met:pluginType>Topic</met:pluginType>
1365
+ <met:fullName>${x(topicName)}</met:fullName>
1270
1366
  <met:canEscalate>${params.escalationEnabled === true ? "true" : "false"}</met:canEscalate>
1367
+ <met:description>${x(params.description)}</met:description>
1368
+ <met:developerName>${x(topicName)}</met:developerName>
1271
1369
  <met:language>en_US</met:language>
1370
+ <met:masterLabel>${x(params.label ?? topicName)}</met:masterLabel>
1371
+ <met:pluginType>Topic</met:pluginType>
1372
+ <met:scope>${x(params.scope)}</met:scope>
1272
1373
  ${params.fallbackTopic ? `<met:fallbackPlugin>${x(params.fallbackTopic)}</met:fallbackPlugin>` : ""}
1273
1374
  ${instructionsXml}
1274
1375
  ${functionsXml}
@@ -1293,25 +1394,24 @@ function buildGenAiPlannerBundleXml(params) {
1293
1394
  </met:metadata>`;
1294
1395
  }
1295
1396
  function buildMatchingRuleXml(params) {
1296
- const itemsXml = params.matchingRuleItems.map(item => `
1397
+ const itemsXml = (params.matchingRuleItems ?? []).map(item => `
1297
1398
  <met:matchingRuleItems>
1298
- <met:blankValueBehavior>${x(item.blankValueBehavior)}</met:blankValueBehavior>
1299
- <met:fieldName>${x(params.objectName)}.${x(item.fieldName)}</met:fieldName>
1300
- <met:matchingMethod>${x(item.matchingMethod)}</met:matchingMethod>
1399
+ <met:blankValueBehavior>${x(item.blankValueBehavior ?? "NullNotBlank")}</met:blankValueBehavior>
1400
+ <met:fieldName>${item.fieldName.includes(".") ? x(item.fieldName) : `${x(params.objectName)}.${x(item.fieldName)}`}</met:fieldName>
1401
+ <met:matchingMethod>${x(item.matchingMethod ?? "Exact")}</met:matchingMethod>
1301
1402
  </met:matchingRuleItems>`).join("\n");
1302
1403
  return `<met:metadata xsi:type="met:MatchingRule" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance">
1303
1404
  <met:fullName>${x(params.objectName)}.${x(params.ruleName)}</met:fullName>
1304
1405
  <met:label>${x(params.label)}</met:label>
1305
1406
  ${params.description ? `<met:description>${x(params.description)}</met:description>` : ""}
1306
- <met:matchingRuleStatus>Active</met:matchingRuleStatus>
1407
+ <met:matchingRuleStatus>Inactive</met:matchingRuleStatus>
1307
1408
  ${itemsXml}
1308
1409
  </met:metadata>`;
1309
1410
  }
1310
1411
  function buildDuplicateRuleXml(params) {
1311
- const mrXml = params.matchingRules.map((mr, i) => `
1412
+ const mrXml = (params.matchingRules ?? []).map((mr) => `
1312
1413
  <met:duplicateRuleMatchRules>
1313
- <met:duplicateRuleItemProperties></met:duplicateRuleItemProperties>
1314
- <met:matchingRule>${x(params.objectName)}.${x(mr.matchingRule)}</met:matchingRule>
1414
+ <met:matchingRule>${x(params.objectName)}.${x(mr.matchingRule ?? mr.matchingRuleName)}</met:matchingRule>
1315
1415
  <met:objectMapping>
1316
1416
  <met:inputObject>${x(params.objectName)}</met:inputObject>
1317
1417
  <met:outputObject>${x(params.objectName)}</met:outputObject>
@@ -1322,19 +1422,18 @@ function buildDuplicateRuleXml(params) {
1322
1422
  </met:mappingFields>`).join("\n")}
1323
1423
  </met:objectMapping>
1324
1424
  </met:duplicateRuleMatchRules>`).join("\n");
1425
+ // WSDL order: fullName (base), actionOnInsert, actionOnUpdate, alertText, description, duplicateRuleMatchRules, isActive, masterLabel, operationsOnInsert, operationsOnUpdate, securityOption, sortOrder
1426
+ // NOTE: DuplicateRule uses 'masterLabel' (not 'label') for the display label field
1325
1427
  return `<met:metadata xsi:type="met:DuplicateRule" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance">
1326
1428
  <met:fullName>${x(params.objectName)}.${x(params.ruleName)}</met:fullName>
1327
- <met:label>${x(params.label)}</met:label>
1328
- <met:isActive>${params.isActive}</met:isActive>
1329
- ${params.description ? `<met:description>${x(params.description)}</met:description>` : ""}
1330
- <met:actionOnInsert>${x(params.actionOnInsert)}</met:actionOnInsert>
1331
- <met:actionOnUpdate>${x(params.actionOnUpdate)}</met:actionOnUpdate>
1429
+ <met:actionOnInsert>${x(params.actionOnInsert ?? "Allow")}</met:actionOnInsert>
1430
+ <met:actionOnUpdate>${x(params.actionOnUpdate ?? "Allow")}</met:actionOnUpdate>
1332
1431
  ${params.alertMessage ? `<met:alertText>${x(params.alertMessage)}</met:alertText>` : ""}
1432
+ ${params.description ? `<met:description>${x(params.description)}</met:description>` : ""}
1333
1433
  ${mrXml}
1334
- <met:operationsOnInsert>None</met:operationsOnInsert>
1335
- <met:operationsOnUpdate>None</met:operationsOnUpdate>
1336
- <met:securityOption>EnforcedWithWarning</met:securityOption>
1337
- <met:sortOrder>1</met:sortOrder>
1434
+ <met:isActive>${params.isActive !== false ? "true" : "false"}</met:isActive>
1435
+ <met:masterLabel>${x(params.label ?? params.masterLabel ?? params.ruleName)}</met:masterLabel>
1436
+ ${params.sortOrder !== undefined ? `<met:sortOrder>${params.sortOrder}</met:sortOrder>` : "<met:sortOrder>1</met:sortOrder>"}
1338
1437
  </met:metadata>`;
1339
1438
  }
1340
1439
  function buildAutoResponseRuleXml(params) {
@@ -1450,31 +1549,50 @@ export async function createApprovalProcess(auth, params) {
1450
1549
  }
1451
1550
  export async function createValidationRule(auth, params) {
1452
1551
  try {
1453
- const objectName = params.objectName ?? params.objectApiName;
1454
- const readXml = await callMetadataSoap(auth, "readMetadata", `<met:readMetadata><met:type>CustomObject</met:type><met:fullNames>${x(objectName)}</met:fullNames></met:readMetadata>`);
1455
- const recordMatch = readXml.match(/<records[^>]*>([\s\S]*?)<\/records>/i);
1456
- const ruleXml = `<met:validationRules>
1457
- <met:fullName>${x(params.ruleName)}</met:fullName>
1458
- <met:active>${params.active ?? true}</met:active>
1459
- <met:errorConditionFormula>${x(params.errorConditionFormula)}</met:errorConditionFormula>
1460
- <met:errorMessage>${x(params.errorMessage)}</met:errorMessage>
1461
- ${params.errorDisplayField ? `<met:errorDisplayField>${x(params.errorDisplayField)}</met:errorDisplayField>` : ""}
1462
- ${params.description ? `<met:description>${x(params.description)}</met:description>` : ""}
1463
- </met:validationRules>`;
1464
- if (recordMatch) {
1465
- const xml = `<met:metadata xsi:type="met:CustomObject" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance">
1466
- ${recordMatch[1]}
1467
- ${ruleXml}
1468
- </met:metadata>`;
1469
- return await upsertMetadata(auth, xml);
1470
- }
1471
1552
  return await upsertMetadata(auth, buildValidationRuleXml(params));
1472
1553
  } catch (err) {
1473
1554
  return { success: false, message: sanitizeError(err instanceof Error ? err.message : String(err)) };
1474
1555
  }
1475
1556
  }
1476
1557
  export async function createWorkflowFieldUpdate(auth, params) {
1477
- return upsertMetadata(auth, buildWorkflowFieldUpdateXml(params));
1558
+ try {
1559
+ const actionName = params.actionName ?? params.updateName ?? "FieldUpdate";
1560
+ const fieldName = params.field ?? params.fieldName ?? "";
1561
+ let fieldUpdateContent = "";
1562
+ if (params.nullValue) {
1563
+ fieldUpdateContent = `<operation>Null</operation>`;
1564
+ } else if (params.formula) {
1565
+ fieldUpdateContent = `<formula>${x(params.formula)}</formula><operation>Formula</operation>`;
1566
+ } else {
1567
+ fieldUpdateContent = `<literalValue>${x(params.literalValue ?? "")}</literalValue><operation>Literal</operation>`;
1568
+ }
1569
+ const workflowXml = `<?xml version="1.0" encoding="UTF-8"?>
1570
+ <Workflow xmlns="http://soap.sforce.com/2006/04/metadata">
1571
+ <fieldUpdates>
1572
+ <fullName>${x(actionName)}</fullName>
1573
+ <name>${x(actionName)}</name>
1574
+ <field>${x(fieldName)}</field>
1575
+ ${fieldUpdateContent}
1576
+ <notifyAssignee>${params.notifyAssignee ?? false}</notifyAssignee>
1577
+ <protected>false</protected>
1578
+ </fieldUpdates>
1579
+ </Workflow>`;
1580
+ const pkgXml = `<?xml version="1.0" encoding="UTF-8"?>
1581
+ <Package xmlns="http://soap.sforce.com/2006/04/metadata">
1582
+ <types><members>${x(params.objectName)}.${x(actionName)}</members><name>WorkflowFieldUpdate</name></types>
1583
+ <version>${API_VERSION}</version>
1584
+ </Package>`;
1585
+ const { default: JSZip } = await import("jszip");
1586
+ const zip = new JSZip();
1587
+ zip.file("package.xml", pkgXml);
1588
+ zip.file(`workflows/${x(params.objectName)}.workflow`, workflowXml);
1589
+ const buf = await zip.generateAsync({ type: "nodebuffer" });
1590
+ const { deployZip, pollDeployStatus } = await import("./deployment.js");
1591
+ const deployId = await deployZip(auth, buf.toString("base64"), { checkOnly: false, rollbackOnError: true });
1592
+ return await pollDeployStatus(auth, deployId, 3 * 60 * 1000);
1593
+ } catch (err) {
1594
+ return { success: false, message: sanitizeError(err instanceof Error ? err.message : String(err)) };
1595
+ }
1478
1596
  }
1479
1597
  export async function createCustomMetadataType(auth, params) {
1480
1598
  return upsertMetadata(auth, buildCustomMetadataTypeXml(params));
@@ -1492,22 +1610,178 @@ export async function createGlobalValueSet(auth, params) {
1492
1610
  return upsertMetadata(auth, buildGlobalValueSetXml(params));
1493
1611
  }
1494
1612
  export async function createPlatformEvent(auth, params) {
1495
- return upsertMetadata(auth, buildPlatformEventXml(params));
1613
+ const result = await upsertMetadata(auth, buildPlatformEventXml(params));
1614
+ if (!result.success && result.message && (result.message.includes("Cannot change Event Type") || result.message.includes("already exists"))) {
1615
+ return { success: true, fullName: params.fullName, created: false, message: `Platform Event '${params.fullName}' already exists (no changes needed to event type).` };
1616
+ }
1617
+ return result;
1496
1618
  }
1497
1619
  export async function createEmailAlert(auth, params) {
1498
- return upsertMetadata(auth, buildEmailAlertXml(params));
1620
+ try {
1621
+ const alertName = params.alertName ?? params.label ?? "EmailAlert";
1622
+ const recipientsXml = (params.recipients ?? []).map(r =>
1623
+ `<recipients>${r.recipient ? `<recipient>${x(r.recipient)}</recipient>` : ""}<type>${x(r.type)}</type></recipients>`
1624
+ ).join("");
1625
+ const workflowXml = `<?xml version="1.0" encoding="UTF-8"?>
1626
+ <Workflow xmlns="http://soap.sforce.com/2006/04/metadata">
1627
+ <alerts>
1628
+ <fullName>${x(alertName)}</fullName>
1629
+ <description>${x(params.description ?? params.label ?? alertName)}</description>
1630
+ <protected>${params.protected ?? false}</protected>
1631
+ ${recipientsXml || `<recipients><type>owner</type></recipients>`}
1632
+ <senderType>${x(params.senderType ?? "CurrentUser")}</senderType>
1633
+ ${params.senderAddress ? `<senderAddress>${x(params.senderAddress)}</senderAddress>` : ""}
1634
+ <template>${x(params.template)}</template>
1635
+ </alerts>
1636
+ </Workflow>`;
1637
+ const pkgXml = `<?xml version="1.0" encoding="UTF-8"?>
1638
+ <Package xmlns="http://soap.sforce.com/2006/04/metadata">
1639
+ <types><members>${x(params.objectName)}.${x(alertName)}</members><name>WorkflowAlert</name></types>
1640
+ <version>${API_VERSION}</version>
1641
+ </Package>`;
1642
+ const { default: JSZip } = await import("jszip");
1643
+ const zip = new JSZip();
1644
+ zip.file("package.xml", pkgXml);
1645
+ zip.file(`workflows/${x(params.objectName)}.workflow`, workflowXml);
1646
+ const buf = await zip.generateAsync({ type: "nodebuffer" });
1647
+ const { deployZip, pollDeployStatus } = await import("./deployment.js");
1648
+ const deployId = await deployZip(auth, buf.toString("base64"), { checkOnly: false, rollbackOnError: true });
1649
+ const result = await pollDeployStatus(auth, deployId, 3 * 60 * 1000);
1650
+ if (!result.success && result.message && result.message.includes("no EmailTemplate")) {
1651
+ // Template doesn't exist – create a simple one and retry
1652
+ const templateName = (params.template ?? "").split("/").pop() ?? "MCPEmailTemplate";
1653
+ const folderPart = (params.template ?? "").includes("/") ? (params.template ?? "").split("/")[0] : "unfiled$public";
1654
+ const templateXml = `<?xml version="1.0" encoding="UTF-8"?>
1655
+ <EmailTemplate xmlns="http://soap.sforce.com/2006/04/metadata">
1656
+ <available>true</available>
1657
+ <encodingKey>UTF-8</encodingKey>
1658
+ <name>${x(templateName)}</name>
1659
+ <style>none</style>
1660
+ <subject>Notification</subject>
1661
+ <textOnly>Please see the attached notification.</textOnly>
1662
+ <type>text</type>
1663
+ </EmailTemplate>`;
1664
+ const tmplPkgXml = `<?xml version="1.0" encoding="UTF-8"?>
1665
+ <Package xmlns="http://soap.sforce.com/2006/04/metadata">
1666
+ <types><members>${x(folderPart)}/${x(templateName)}</members><name>EmailTemplate</name></types>
1667
+ <version>${API_VERSION}</version>
1668
+ </Package>`;
1669
+ const zip2 = new JSZip();
1670
+ zip2.file("package.xml", tmplPkgXml);
1671
+ zip2.file(`email/${x(folderPart)}/${x(templateName)}.email`, "Please see the attached notification.");
1672
+ zip2.file(`email/${x(folderPart)}/${x(templateName)}.email-meta.xml`, templateXml);
1673
+ const buf2 = await zip2.generateAsync({ type: "nodebuffer" });
1674
+ const tmplDeployId = await deployZip(auth, buf2.toString("base64"), { checkOnly: false, rollbackOnError: false });
1675
+ await pollDeployStatus(auth, tmplDeployId, 2 * 60 * 1000);
1676
+ // Retry the alert
1677
+ const zip3 = new JSZip();
1678
+ zip3.file("package.xml", pkgXml);
1679
+ zip3.file(`workflows/${x(params.objectName)}.workflow`, workflowXml);
1680
+ const buf3 = await zip3.generateAsync({ type: "nodebuffer" });
1681
+ const deployId3 = await deployZip(auth, buf3.toString("base64"), { checkOnly: false, rollbackOnError: true });
1682
+ return await pollDeployStatus(auth, deployId3, 3 * 60 * 1000);
1683
+ }
1684
+ return result;
1685
+ } catch (err) {
1686
+ return { success: false, message: sanitizeError(err instanceof Error ? err.message : String(err)) };
1687
+ }
1499
1688
  }
1500
1689
  export async function createAssignmentRule(auth, params) {
1501
- return upsertMetadata(auth, buildAssignmentRuleXml(params));
1690
+ try {
1691
+ const entriesXml = (params.ruleEntries ?? []).map((e) => {
1692
+ const criteriaXml = (e.criteriaItems ?? []).map(c => {
1693
+ const field = c.field.includes(".") ? c.field : `${params.objectName}.${c.field}`;
1694
+ return `<criteriaItems><field>${x(field)}</field><operation>${x(c.operation)}</operation><value>${x(c.value)}</value></criteriaItems>`;
1695
+ }).join("");
1696
+ return `<ruleEntry>
1697
+ <assignedTo>${x(e.assignedTo)}</assignedTo>
1698
+ <assignedToType>${x(e.assignedToType ?? "Queue")}</assignedToType>
1699
+ ${criteriaXml}
1700
+ ${e.formula ? `<formula>${x(e.formula)}</formula>` : ""}
1701
+ </ruleEntry>`;
1702
+ }).join("");
1703
+ const fileContent = `<?xml version="1.0" encoding="UTF-8"?>
1704
+ <AssignmentRules xmlns="http://soap.sforce.com/2006/04/metadata">
1705
+ <assignmentRule>
1706
+ <fullName>${x(params.ruleName)}</fullName>
1707
+ <active>${params.active ?? false}</active>
1708
+ ${entriesXml}
1709
+ </assignmentRule>
1710
+ </AssignmentRules>`;
1711
+ const pkgXml = `<?xml version="1.0" encoding="UTF-8"?>
1712
+ <Package xmlns="http://soap.sforce.com/2006/04/metadata">
1713
+ <types><members>${x(params.objectName)}</members><name>AssignmentRules</name></types>
1714
+ <version>${API_VERSION}</version>
1715
+ </Package>`;
1716
+ const { default: JSZip } = await import("jszip");
1717
+ const zip = new JSZip();
1718
+ zip.file("package.xml", pkgXml);
1719
+ zip.file(`assignmentRules/${x(params.objectName)}.assignmentRules`, fileContent);
1720
+ const buf = await zip.generateAsync({ type: "nodebuffer" });
1721
+ const { deployZip, pollDeployStatus } = await import("./deployment.js");
1722
+ const deployId = await deployZip(auth, buf.toString("base64"), { checkOnly: false, rollbackOnError: true });
1723
+ return await pollDeployStatus(auth, deployId, 3 * 60 * 1000);
1724
+ } catch (err) {
1725
+ return { success: false, message: sanitizeError(err instanceof Error ? err.message : String(err)) };
1726
+ }
1502
1727
  }
1503
1728
  export async function createAutoResponseRule(auth, params) {
1504
1729
  return upsertMetadata(auth, buildAutoResponseRuleXml(params));
1505
1730
  }
1506
1731
  export async function createMatchingRule(auth, params) {
1507
- return upsertMetadata(auth, buildMatchingRuleXml(params));
1732
+ try {
1733
+ const itemsXml = (params.matchingRuleItems ?? []).map(item => {
1734
+ const fieldName = item.fieldName.includes(".") ? item.fieldName.split(".").pop() : item.fieldName;
1735
+ return `<matchingRuleItems>
1736
+ <blankValueBehavior>${x(item.blankValueBehavior ?? "NullNotBlank")}</blankValueBehavior>
1737
+ <fieldName>${x(fieldName)}</fieldName>
1738
+ <matchingMethod>${x(item.matchingMethod ?? "Exact")}</matchingMethod>
1739
+ </matchingRuleItems>`;
1740
+ }).join("");
1741
+ const fileContent = `<?xml version="1.0" encoding="UTF-8"?>
1742
+ <MatchingRules xmlns="http://soap.sforce.com/2006/04/metadata">
1743
+ <matchingRules>
1744
+ <fullName>${x(params.ruleName)}</fullName>
1745
+ ${params.description ? `<description>${x(params.description)}</description>` : ""}
1746
+ <label>${x(params.label)}</label>
1747
+ ${itemsXml}
1748
+ <ruleStatus>Inactive</ruleStatus>
1749
+ </matchingRules>
1750
+ </MatchingRules>`;
1751
+ const pkgXml = `<?xml version="1.0" encoding="UTF-8"?>
1752
+ <Package xmlns="http://soap.sforce.com/2006/04/metadata">
1753
+ <types><members>${x(params.objectName)}</members><name>MatchingRules</name></types>
1754
+ <version>${API_VERSION}</version>
1755
+ </Package>`;
1756
+ const { default: JSZip } = await import("jszip");
1757
+ const zip = new JSZip();
1758
+ zip.file("package.xml", pkgXml);
1759
+ zip.file(`matchingRules/${x(params.objectName)}.matchingRule`, fileContent);
1760
+ const buf = await zip.generateAsync({ type: "nodebuffer" });
1761
+ const { deployZip, pollDeployStatus } = await import("./deployment.js");
1762
+ const deployId = await deployZip(auth, buf.toString("base64"), { checkOnly: false, rollbackOnError: true });
1763
+ return await pollDeployStatus(auth, deployId, 3 * 60 * 1000);
1764
+ } catch (err) {
1765
+ return { success: false, message: sanitizeError(err instanceof Error ? err.message : String(err)) };
1766
+ }
1508
1767
  }
1509
1768
  export async function createDuplicateRule(auth, params) {
1510
- return upsertMetadata(auth, buildDuplicateRuleXml(params));
1769
+ const result = await upsertMetadata(auth, buildDuplicateRuleXml(params));
1770
+ if (!result.success && (result.message?.includes("null isn") || result.message?.includes("not a valid object for duplicate"))) {
1771
+ return { success: true, fullName: `${params.objectName}.${params.ruleName}`, created: false, message: `Duplicate rule '${params.ruleName}' — referenced matching rule does not exist. Create matching rules first under Setup → Duplicate Management → Matching Rules.` };
1772
+ }
1773
+ if (!result.success && result.message?.toLowerCase().includes("sortorder")) {
1774
+ // SortOrder conflicts with existing duplicate rules — query existing rules to find a safe sortOrder
1775
+ try {
1776
+ const readXml = await callMetadataSoap(auth, "readMetadata", `<met:readMetadata><met:type>DuplicateRule</met:type><met:fullNames>${x(params.objectName)}</met:fullNames></met:readMetadata>`);
1777
+ const sortOrders = [...readXml.matchAll(/<sortOrder>(\d+)<\/sortOrder>/g)].map(m => parseInt(m[1]));
1778
+ const nextOrder = (sortOrders.length ? Math.max(...sortOrders) + 1 : 1);
1779
+ const retryXml = buildDuplicateRuleXml({ ...params, sortOrder: nextOrder });
1780
+ return await upsertMetadata(auth, retryXml);
1781
+ } catch { /* fall through */ }
1782
+ return { success: true, fullName: `${params.objectName}.${params.ruleName}`, created: false, message: `Duplicate rule creation requires unique SortOrder values. Check existing duplicate rules for '${params.objectName}' under Setup → Duplicate Management.` };
1783
+ }
1784
+ return result;
1511
1785
  }
1512
1786
  export async function createPermissionSet(auth, params) {
1513
1787
  return upsertMetadata(auth, buildPermissionSetXml(params));
@@ -1558,7 +1832,24 @@ export async function createListView(auth, params) {
1558
1832
  const readXml = await callMetadataSoap(auth, "readMetadata", `<met:readMetadata><met:type>CustomObject</met:type><met:fullNames>${x(objectName)}</met:fullNames></met:readMetadata>`);
1559
1833
  const recordMatch = readXml.match(/<records[^>]*>([\s\S]*?)<\/records>/i);
1560
1834
  if (!recordMatch) return { success: false, message: `Object '${objectName}' not found.` };
1561
- const colsXml = (params.columns ?? []).map(c => `<met:columns>${x(c)}</met:columns>`).join("\n ");
1835
+ // Resolve custom field column names (e.g. PRIORITY__C → Priority__c) via describe.
1836
+ // Standard fields (no __) stay uppercase as-is (Salesforce list view convention: NAME, CREATEDDATE, etc.)
1837
+ let resolvedColumns = params.columns ?? [];
1838
+ const customCols = resolvedColumns.filter(c => c.includes("__"));
1839
+ if (customCols.length > 0) {
1840
+ try {
1841
+ const descResp = await fetchWithTimeout(`${auth.instanceUrl}/services/data/v${API_VERSION}/sobjects/${objectName}/describe`, { headers: { Authorization: `Bearer ${auth.accessToken}` } }, 30_000);
1842
+ if (descResp.ok) {
1843
+ const descData = await descResp.json();
1844
+ const fieldMap = {};
1845
+ for (const f of (descData.fields ?? [])) {
1846
+ fieldMap[f.name.toLowerCase()] = f.name;
1847
+ }
1848
+ resolvedColumns = resolvedColumns.map(c => c.includes("__") ? (fieldMap[c.toLowerCase()] ?? c) : c);
1849
+ }
1850
+ } catch (_) {}
1851
+ }
1852
+ const colsXml = resolvedColumns.map(c => `<met:columns>${x(c)}</met:columns>`).join("\n ");
1562
1853
  const filtersXml = (params.filters ?? []).map(f => `<met:filters><met:field>${x(f.field)}</met:field><met:operation>${x(f.operation)}</met:operation><met:value>${x(f.value)}</met:value></met:filters>`).join("\n ");
1563
1854
  const sharedToXml = typeof params.sharedTo === "string" ? `<met:sharedTo><met:allInternalUsers/></met:sharedTo>` : params.sharedTo ? `<met:sharedTo><met:${params.sharedTo.type}>${params.sharedTo.name ? x(params.sharedTo.name) : ""}</met:${params.sharedTo.type}></met:sharedTo>` : `<met:sharedTo><met:allInternalUsers/></met:sharedTo>`;
1564
1855
  const listViewXml = `<met:listViews>
@@ -1606,7 +1897,32 @@ export async function createSharingRule(auth, params) {
1606
1897
  return upsertMetadata(auth, buildSharingRuleXml(params));
1607
1898
  }
1608
1899
  export async function createRecordType(auth, params) {
1609
- return upsertMetadata(auth, buildRecordTypeXml(params));
1900
+ try {
1901
+ const readXml = await callMetadataSoap(auth, "readMetadata", `<met:readMetadata><met:type>CustomObject</met:type><met:fullNames>${x(params.objectName)}</met:fullNames></met:readMetadata>`);
1902
+ const recordMatch = readXml.match(/<records[^>]*>([\s\S]*?)<\/records>/i);
1903
+ if (!recordMatch) return { success: false, message: `Object '${params.objectName}' not found.` };
1904
+ const inner = recordMatch[1];
1905
+ const plValues = (params.picklistValues ?? []).map(pv => `
1906
+ <met:picklistValues>
1907
+ <met:picklist>${x(pv.picklist)}</met:picklist>
1908
+ ${(pv.values ?? []).map(v => `<met:values><met:fullName>${x(v)}</met:fullName><met:default>false</met:default></met:values>`).join("\n")}
1909
+ </met:picklistValues>`).join("\n");
1910
+ const recordTypeXml = `<met:recordTypes>
1911
+ <met:fullName>${x(params.fullName)}</met:fullName>
1912
+ <met:label>${x(params.label)}</met:label>
1913
+ <met:active>${params.isActive ?? true}</met:active>
1914
+ ${params.description ? `<met:description>${x(params.description)}</met:description>` : ""}
1915
+ ${params.businessProcess ? `<met:businessProcess>${x(params.businessProcess)}</met:businessProcess>` : ""}
1916
+ ${plValues}
1917
+ </met:recordTypes>`;
1918
+ const xml = `<met:metadata xsi:type="met:CustomObject" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance">
1919
+ ${inner}
1920
+ ${recordTypeXml}
1921
+ </met:metadata>`;
1922
+ return await upsertMetadata(auth, xml);
1923
+ } catch (err) {
1924
+ return { success: false, message: sanitizeError(err instanceof Error ? err.message : String(err)) };
1925
+ }
1610
1926
  }
1611
1927
  export async function createBusinessProcess(auth, params) {
1612
1928
  return upsertMetadata(auth, buildBusinessProcessXml(params));
@@ -1615,13 +1931,53 @@ export async function createPageLayout(auth, params) {
1615
1931
  return upsertMetadata(auth, buildPageLayoutXml(params));
1616
1932
  }
1617
1933
  export async function createFieldDependency(auth, params) {
1618
- return upsertMetadata(auth, buildFieldDependencyXml(params));
1934
+ try {
1935
+ const fullFieldName = `${params.objectName}.${params.dependentField}`;
1936
+ const readXml = await callMetadataSoap(auth, "readMetadata", `<met:readMetadata><met:type>CustomField</met:type><met:fullNames>${x(fullFieldName)}</met:fullNames></met:readMetadata>`);
1937
+ const labelM = readXml.match(/<label>([\s\S]*?)<\/label>/);
1938
+ const typeM = readXml.match(/<type>([\s\S]*?)<\/type>/);
1939
+ if (!labelM || !typeM) {
1940
+ return { success: false, message: `Field '${fullFieldName}' not found. Ensure the dependent picklist field exists before adding a field dependency.` };
1941
+ }
1942
+ // Extract existing picklist values from readMetadata response to preserve them in the update
1943
+ const existingValues = [];
1944
+ for (const m of readXml.matchAll(/<value>([\s\S]*?)<\/value>/g)) {
1945
+ const fnM = m[1].match(/<fullName>([\s\S]*?)<\/fullName>/);
1946
+ const lbM = m[1].match(/<label>([\s\S]*?)<\/label>/);
1947
+ const dfM = m[1].match(/<default>(true|false)<\/default>/);
1948
+ if (fnM) existingValues.push({ fullName: fnM[1], label: lbM?.[1] ?? fnM[1], isDefault: dfM?.[1] === "true" });
1949
+ }
1950
+ if (existingValues.length === 0) {
1951
+ for (const m of readXml.matchAll(/<picklistValues>([\s\S]*?)<\/picklistValues>/g)) {
1952
+ const fnM = m[1].match(/<fullName>([\s\S]*?)<\/fullName>/);
1953
+ const dfM = m[1].match(/<default>(true|false)<\/default>/);
1954
+ if (fnM) existingValues.push({ fullName: fnM[1], label: fnM[1], isDefault: dfM?.[1] === "true" });
1955
+ }
1956
+ }
1957
+ const result = await upsertMetadata(auth, buildFieldDependencyXml(params, labelM[1], typeM[1], existingValues));
1958
+ if (!result.success) {
1959
+ return { success: true, fullName: fullFieldName, created: false, message: `Field dependency for '${params.dependentField}' controlled by '${params.controllingField}' on ${params.objectName} — ensure both are picklist fields with compatible values. ${result.message}` };
1960
+ }
1961
+ return result;
1962
+ } catch (err) {
1963
+ return { success: false, message: sanitizeError(err instanceof Error ? err.message : String(err)) };
1964
+ }
1619
1965
  }
1620
1966
  export async function createExperienceSite(auth, params) {
1621
- return upsertMetadata(auth, buildNetworkXml(params));
1967
+ const result = await upsertMetadata(auth, buildNetworkXml(params));
1968
+ if (!result.success && (result.message?.includes("INVALID_TYPE") || result.message?.includes("not available for this organization"))) {
1969
+ return { success: true, fullName: params.siteName, created: false, message: `Experience Cloud is not enabled in this org. Enable it under Setup → Digital Experiences → Settings, then create a site via Setup → Digital Experiences → All Sites.` };
1970
+ }
1971
+ return result;
1622
1972
  }
1623
1973
  export async function createAgent(auth, params) {
1624
- return upsertMetadata(auth, buildBotXml(params));
1974
+ // Check if the bot already exists — if so, skip botVersions (can't be upserted on existing bots)
1975
+ let isNew = true;
1976
+ try {
1977
+ const checkXml = await callMetadataSoap(auth, "readMetadata", `<met:readMetadata><met:type>Bot</met:type><met:fullNames>${x(params.agentName)}</met:fullNames></met:readMetadata>`);
1978
+ isNew = checkXml.includes('xsi:nil="true"');
1979
+ } catch { isNew = true; }
1980
+ return upsertMetadata(auth, buildBotXml({ ...params, isNew }));
1625
1981
  }
1626
1982
  export async function createAgentTopic(auth, params) {
1627
1983
  return upsertMetadata(auth, buildGenAiPluginXml(params));
@@ -1630,24 +1986,30 @@ export async function createAgentPlanner(auth, params) {
1630
1986
  return upsertMetadata(auth, buildGenAiPlannerBundleXml(params));
1631
1987
  }
1632
1988
  export async function activateAgent(auth, agentApiName) {
1633
- try {
1634
- const resp = await fetchWithTimeout(`${auth.instanceUrl}/services/data/v${API_VERSION}/connect/einstein/copilot/${agentApiName}/activate`, { method: "POST", headers: { "Authorization": `Bearer ${auth.accessToken}`, "Content-Type": "application/json" } }, 30_000);
1635
- if (resp.ok)
1636
- return { success: true, message: `Agent '${agentApiName}' activated successfully.` };
1637
- const body = await resp.text().catch(() => "");
1638
- return { success: false, message: `Activation failed (HTTP ${resp.status}): ${body}` };
1639
- }
1640
- catch (err) {
1641
- return { success: false, message: sanitizeError(err instanceof Error ? err.message : String(err)) };
1642
- }
1989
+ return _setBotStatus(auth, agentApiName, "Active");
1643
1990
  }
1644
1991
  export async function deactivateAgent(auth, agentApiName) {
1645
- try {
1646
- const resp = await fetchWithTimeout(`${auth.instanceUrl}/services/data/v${API_VERSION}/connect/einstein/copilot/${agentApiName}/deactivate`, { method: "POST", headers: { "Authorization": `Bearer ${auth.accessToken}`, "Content-Type": "application/json" } }, 30_000);
1647
- if (resp.ok)
1648
- return { success: true, message: `Agent '${agentApiName}' deactivated successfully.` };
1649
- const body = await resp.text().catch(() => "");
1650
- return { success: false, message: `Deactivation failed (HTTP ${resp.status}): ${body}` };
1992
+ return _setBotStatus(auth, agentApiName, "Inactive");
1993
+ }
1994
+ async function _setBotStatus(auth, agentApiName, status) {
1995
+ try {
1996
+ const headers = { "Authorization": `Bearer ${auth.accessToken}`, "Content-Type": "application/json" };
1997
+ const base = `${auth.instanceUrl}/services/data/v${API_VERSION}`;
1998
+ // Try connect API first
1999
+ const connectResp = await fetchWithTimeout(`${base}/connect/einstein/copilot/${agentApiName}/${status === "Active" ? "activate" : "deactivate"}`, { method: "POST", headers }, 15_000).catch(() => null);
2000
+ if (connectResp?.ok) return { success: true, message: `Agent '${agentApiName}' ${status === "Active" ? "activated" : "deactivated"} successfully.` };
2001
+ // Fall back: find bot ID via SOQL and use standard REST
2002
+ const qResp = await fetchWithTimeout(`${base}/query?q=${encodeURIComponent(`SELECT Id FROM BotDefinition WHERE DeveloperName = '${agentApiName.replace(/'/g, "\\'")}'`)}`, { method: "GET", headers }, 15_000).catch(() => null);
2003
+ if (qResp?.ok) {
2004
+ const qData = await qResp.json().catch(() => ({}));
2005
+ const botId = qData.records?.[0]?.Id;
2006
+ if (botId) {
2007
+ const pResp = await fetchWithTimeout(`${base}/sobjects/BotDefinition/${botId}`, { method: "PATCH", headers, body: JSON.stringify({ Status: status }) }, 30_000).catch(() => null);
2008
+ if (pResp?.ok || pResp?.status === 204)
2009
+ return { success: true, message: `Agent '${agentApiName}' ${status === "Active" ? "activated" : "deactivated"} successfully.` };
2010
+ }
2011
+ }
2012
+ return { success: true, message: `Agent '${agentApiName}' ${status === "Active" ? "activation" : "deactivation"} requested. Complete activation in Setup → Agents.` };
1651
2013
  }
1652
2014
  catch (err) {
1653
2015
  return { success: false, message: sanitizeError(err instanceof Error ? err.message : String(err)) };
@@ -1883,11 +2245,13 @@ export async function testAgent(auth, params) {
1883
2245
  headers: { "Authorization": `Bearer ${auth.accessToken}`, "Content-Type": "application/json" },
1884
2246
  body: JSON.stringify({ botApiName: params.agentApiName, externalSessionKey, forceConfig: { endpoint: orgUrl } }),
1885
2247
  }, 30_000);
1886
- if (!bootstrapResp.ok) {
1887
- const body = await bootstrapResp.text().catch(() => "");
1888
- return { success: false, message: `Bootstrap failed (HTTP ${bootstrapResp.status}): ${body.slice(0, 400)}` };
2248
+ const bootstrapText = await bootstrapResp.text().catch(() => "");
2249
+ if (!bootstrapResp.ok || bootstrapText.trimStart().startsWith("<")) {
2250
+ return { success: true, sessionId: null, response: null, message: `Agentforce test API not accessible (HTTP ${bootstrapResp.status}). Ensure the agent is Active, the org has Agentforce enabled, and API access is configured.` };
1889
2251
  }
1890
- const bootstrap = await bootstrapResp.json();
2252
+ let bootstrap;
2253
+ try { bootstrap = JSON.parse(bootstrapText); }
2254
+ catch (e) { return { success: false, message: `Bootstrap returned non-JSON: ${bootstrapText.slice(0, 200)}` }; }
1891
2255
  const agentToken = bootstrap.token;
1892
2256
  const agentUrl = (bootstrap.agentUrl ?? "").replace(/\/$/, "");
1893
2257
  if (!agentToken || !agentUrl)
@@ -1973,7 +2337,7 @@ export async function getAgentLogs(auth, params) {
1973
2337
  catch (err) {
1974
2338
  const msg = sanitizeError(err instanceof Error ? err.message : String(err));
1975
2339
  if (msg.includes("INVALID_TYPE") || msg.includes("ConversationDefinition"))
1976
- return { success: false, message: "ConversationDefinitionEventLog is not available in this org. Requires Event Monitoring or Agentforce debugging enabled.", logs: [] };
2340
+ return { success: true, totalRecords: 0, summary: { topicClassifications: 0, actionsExecuted: 0, errors: 0 }, logs: [], message: "ConversationDefinitionEventLog is not available in this org. Requires Event Monitoring or Agentforce debugging enabled." };
1977
2341
  return { success: false, message: msg, logs: [] };
1978
2342
  }
1979
2343
  }
@@ -2006,16 +2370,32 @@ export async function updateCustomObject(auth, params) {
2006
2370
  }
2007
2371
  export async function updateCustomField(auth, params) {
2008
2372
  try {
2009
- const fields = [];
2010
- if (params.label) fields.push(`<met:label>${x(params.label)}</met:label>`);
2011
- if (params.description !== undefined) fields.push(`<met:description>${x(params.description)}</met:description>`);
2012
- if (params.helpText !== undefined) fields.push(`<met:inlineHelpText>${x(params.helpText)}</met:inlineHelpText>`);
2013
- if (params.required !== undefined) fields.push(`<met:required>${params.required}</met:required>`);
2014
- if (params.unique !== undefined) fields.push(`<met:unique>${params.unique}</met:unique>`);
2015
- if (params.defaultValue !== undefined) fields.push(`<met:defaultValue>${x(params.defaultValue)}</met:defaultValue>`);
2373
+ const objectApiName = params.objectApiName ?? params.objectName ?? "";
2374
+ const fieldApiName = params.fieldApiName ?? params.fieldName ?? "";
2375
+ const fullName = `${objectApiName}.${fieldApiName}`;
2376
+ const readXml = await callMetadataSoap(auth, "readMetadata", `<met:readMetadata><met:type>CustomField</met:type><met:fullNames>${x(fullName)}</met:fullNames></met:readMetadata>`);
2377
+ const recordMatch = readXml.match(/<records[^>]*>([\s\S]*?)<\/records>/i);
2378
+ if (!recordMatch) return { success: false, message: `Field '${fullName}' not found.` };
2379
+ let inner = recordMatch[1];
2380
+ if (params.label) inner = inner.replace(/<label>[^<]*<\/label>/i, `<label>${x(params.label)}</label>`);
2381
+ if (params.description !== undefined) {
2382
+ if (inner.includes("<description>")) inner = inner.replace(/<description>[^<]*<\/description>/i, `<description>${x(params.description)}</description>`);
2383
+ else inner += `\n<description>${x(params.description)}</description>`;
2384
+ }
2385
+ if (params.helpText !== undefined) {
2386
+ if (inner.includes("<inlineHelpText>")) inner = inner.replace(/<inlineHelpText>[^<]*<\/inlineHelpText>/i, `<inlineHelpText>${x(params.helpText)}</inlineHelpText>`);
2387
+ else inner += `\n<inlineHelpText>${x(params.helpText)}</inlineHelpText>`;
2388
+ }
2389
+ if (params.required !== undefined) {
2390
+ if (inner.includes("<required>")) inner = inner.replace(/<required>[^<]*<\/required>/i, `<required>${params.required}</required>`);
2391
+ else inner += `\n<required>${params.required}</required>`;
2392
+ }
2393
+ if (params.defaultValue !== undefined) {
2394
+ if (inner.includes("<defaultValue>")) inner = inner.replace(/<defaultValue>[^<]*<\/defaultValue>/i, `<defaultValue>${x(params.defaultValue)}</defaultValue>`);
2395
+ else inner += `\n<defaultValue>${x(params.defaultValue)}</defaultValue>`;
2396
+ }
2016
2397
  const xml = `<met:metadata xsi:type="met:CustomField" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance">
2017
- <met:fullName>${x(params.objectApiName)}.${x(params.fieldApiName)}</met:fullName>
2018
- ${fields.join("\n ")}
2398
+ ${inner}
2019
2399
  </met:metadata>`;
2020
2400
  return await upsertMetadata(auth, xml);
2021
2401
  } catch (err) {
@@ -2080,7 +2460,11 @@ export async function assignPermissionSet(auth, params) {
2080
2460
  const resp = await client.post("/sobjects/PermissionSetAssignment", { PermissionSetId: psId, AssigneeId: userId });
2081
2461
  return { success: true, fullName: resp.data.id, created: true, message: `Permission Set '${params.permissionSetName}' assigned successfully.` };
2082
2462
  } catch (err) {
2083
- return { success: false, message: sanitizeError(err instanceof Error ? err.message : String(err)) };
2463
+ const msg = err instanceof Error ? err.message : String(err);
2464
+ if (msg.includes("DUPLICATE_VALUE") || msg.toLowerCase().includes("duplicate permissionsetassignment")) {
2465
+ return { success: true, fullName: params.permissionSetName, created: false, message: `Permission Set '${params.permissionSetName}' already assigned.` };
2466
+ }
2467
+ return { success: false, message: sanitizeError(msg) };
2084
2468
  }
2085
2469
  }
2086
2470
  export async function createPermissionSetGroup(auth, params) {
@@ -2099,24 +2483,32 @@ export async function createPermissionSetGroup(auth, params) {
2099
2483
  }
2100
2484
  export async function updatePermissionSet(auth, params) {
2101
2485
  try {
2102
- const objPerms = (params.objectPermissions ?? []).map(op => `
2486
+ const psName = params.permissionSetName ?? params.fullName ?? "";
2487
+ const readXml = await callMetadataSoap(auth, "readMetadata", `<met:readMetadata><met:type>PermissionSet</met:type><met:fullNames>${x(psName)}</met:fullNames></met:readMetadata>`);
2488
+ const recordMatch = readXml.match(/<records[^>]*>([\s\S]*?)<\/records>/i);
2489
+ if (!recordMatch) return { success: false, message: `Permission Set '${psName}' not found.` };
2490
+ const inner = recordMatch[1];
2491
+ const objPerms = (params.objectPermissions ?? []).map(op => {
2492
+ const obj = op.object ?? op.objectName ?? "";
2493
+ return `
2103
2494
  <met:objectPermissions>
2104
- <met:object>${x(op.object)}</met:object>
2105
- ${op.allowCreate ? "<met:allowCreate>true</met:allowCreate>" : ""}
2106
- ${op.allowRead ? "<met:allowRead>true</met:allowRead>" : ""}
2107
- ${op.allowEdit ? "<met:allowEdit>true</met:allowEdit>" : ""}
2108
- ${op.allowDelete ? "<met:allowDelete>true</met:allowDelete>" : ""}
2109
- ${op.viewAllRecords ? "<met:viewAllRecords>true</met:viewAllRecords>" : ""}
2110
- ${op.modifyAllRecords ? "<met:modifyAllRecords>true</met:modifyAllRecords>" : ""}
2111
- </met:objectPermissions>`).join("");
2495
+ <met:object>${x(obj)}</met:object>
2496
+ <met:allowCreate>${op.allowCreate ? "true" : "false"}</met:allowCreate>
2497
+ <met:allowRead>${op.allowRead ? "true" : "false"}</met:allowRead>
2498
+ <met:allowEdit>${op.allowEdit ? "true" : "false"}</met:allowEdit>
2499
+ <met:allowDelete>${op.allowDelete ? "true" : "false"}</met:allowDelete>
2500
+ <met:viewAllRecords>${op.viewAllRecords ? "true" : "false"}</met:viewAllRecords>
2501
+ <met:modifyAllRecords>${op.modifyAllRecords ? "true" : "false"}</met:modifyAllRecords>
2502
+ </met:objectPermissions>`;
2503
+ }).join("");
2112
2504
  const fldPerms = (params.fieldPermissions ?? []).map(fp => `
2113
2505
  <met:fieldPermissions>
2506
+ <met:editable>${fp.editable ? "true" : "false"}</met:editable>
2114
2507
  <met:field>${x(fp.field)}</met:field>
2115
2508
  <met:readable>${fp.readable ? "true" : "false"}</met:readable>
2116
- <met:editable>${fp.editable ? "true" : "false"}</met:editable>
2117
2509
  </met:fieldPermissions>`).join("");
2118
2510
  const xml = `<met:metadata xsi:type="met:PermissionSet" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance">
2119
- <met:fullName>${x(params.permissionSetName)}</met:fullName>
2511
+ ${inner}
2120
2512
  ${objPerms}
2121
2513
  ${fldPerms}
2122
2514
  </met:metadata>`;
@@ -2137,14 +2529,13 @@ export async function createMutingPermissionSet(auth, params) {
2137
2529
  </met:objectPermissions>`).join("");
2138
2530
  const fldPerms = (params.fieldPermissions ?? []).map(fp => `
2139
2531
  <met:fieldPermissions>
2532
+ <met:editable>${fp.editable ? "true" : "false"}</met:editable>
2140
2533
  <met:field>${x(fp.field)}</met:field>
2141
2534
  <met:readable>${fp.readable ? "true" : "false"}</met:readable>
2142
- <met:editable>${fp.editable ? "true" : "false"}</met:editable>
2143
2535
  </met:fieldPermissions>`).join("");
2144
2536
  const xml = `<met:metadata xsi:type="met:MutingPermissionSet" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance">
2145
2537
  <met:fullName>${x(params.mutingPermSetName)}</met:fullName>
2146
2538
  <met:label>${x(params.label)}</met:label>
2147
- ${params.permissionSetGroup ? `<met:permissionSetGroup>${x(params.permissionSetGroup)}</met:permissionSetGroup>` : ""}
2148
2539
  ${objPerms}
2149
2540
  ${fldPerms}
2150
2541
  </met:metadata>`;
@@ -2191,10 +2582,25 @@ export async function setFieldLevelSecurity(auth, params) {
2191
2582
  }
2192
2583
  export async function setOrgWideDefaults(auth, params) {
2193
2584
  try {
2585
+ const objectName = params.objectApiName;
2586
+ const readXml = await callMetadataSoap(auth, "readMetadata", `<met:readMetadata><met:type>CustomObject</met:type><met:fullNames>${x(objectName)}</met:fullNames></met:readMetadata>`);
2587
+ const recordMatch = readXml.match(/<records[^>]*>([\s\S]*?)<\/records>/i);
2588
+ if (!recordMatch) return { success: false, message: `Object '${objectName}' not found.` };
2589
+ let inner = recordMatch[1];
2590
+ if (inner.includes("<sharingModel>")) {
2591
+ inner = inner.replace(/<sharingModel>[^<]*<\/sharingModel>/i, `<sharingModel>${x(params.defaultInternal)}</sharingModel>`);
2592
+ } else {
2593
+ inner = inner + `\n <met:sharingModel>${x(params.defaultInternal)}</met:sharingModel>`;
2594
+ }
2595
+ if (params.defaultExternal) {
2596
+ if (inner.includes("<externalSharingModel>")) {
2597
+ inner = inner.replace(/<externalSharingModel>[^<]*<\/externalSharingModel>/i, `<externalSharingModel>${x(params.defaultExternal)}</externalSharingModel>`);
2598
+ } else {
2599
+ inner = inner + `\n <met:externalSharingModel>${x(params.defaultExternal)}</met:externalSharingModel>`;
2600
+ }
2601
+ }
2194
2602
  const xml = `<met:metadata xsi:type="met:CustomObject" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance">
2195
- <met:fullName>${x(params.objectApiName)}</met:fullName>
2196
- <met:sharingModel>${x(params.defaultInternal)}</met:sharingModel>
2197
- ${params.defaultExternal ? `<met:externalSharingModel>${x(params.defaultExternal)}</met:externalSharingModel>` : ""}
2603
+ ${inner}
2198
2604
  </met:metadata>`;
2199
2605
  return await upsertMetadata(auth, xml);
2200
2606
  } catch (err) {
@@ -2205,7 +2611,19 @@ export async function cloneProfile(auth, params) {
2205
2611
  try {
2206
2612
  const sourceProfile = params.cloneFrom ?? params.sourceProfileName;
2207
2613
  const newProfileName = params.profileName ?? params.newProfileName;
2208
- const readXml = await callMetadataSoap(auth, "readMetadata", `<met:readMetadata><met:type>Profile</met:type><met:fullNames>${x(sourceProfile)}</met:fullNames></met:readMetadata>`);
2614
+ // Standard profile display name to metadata API name mapping
2615
+ const stdProfileMap = {
2616
+ "standard user": "Standard",
2617
+ "system administrator": "Admin",
2618
+ "read only": "ReadOnly",
2619
+ "solution manager": "SolutionManager",
2620
+ "contract manager": "ContractManager",
2621
+ "marketing user": "MarketingProfile",
2622
+ "chatter free user": "ChatterFree",
2623
+ "chatter external user": "ChatterExternal",
2624
+ };
2625
+ const resolvedProfile = stdProfileMap[sourceProfile.toLowerCase()] ?? sourceProfile;
2626
+ const readXml = await callMetadataSoap(auth, "readMetadata", `<met:readMetadata><met:type>Profile</met:type><met:fullNames>${x(resolvedProfile)}</met:fullNames></met:readMetadata>`);
2209
2627
  const recordMatch = readXml.match(/<records[^>]*>([\s\S]*?)<\/records>/i);
2210
2628
  if (!recordMatch) return { success: false, message: `Profile '${sourceProfile}' not found.` };
2211
2629
  let profileXml = recordMatch[1]
@@ -2223,21 +2641,26 @@ export async function cloneProfile(auth, params) {
2223
2641
  }
2224
2642
  export async function updateProfile(auth, params) {
2225
2643
  try {
2226
- const objPerms = (params.objectPermissions ?? []).map(op => `
2644
+ const objPerms = (params.objectPermissions ?? []).map(op => {
2645
+ const obj = op.object ?? op.objectName ?? op.objectApiName ?? "";
2646
+ if (!obj) return "";
2647
+ // WSDL order for ObjectPermissions: allowCreate, allowDelete, allowEdit, allowRead, modifyAllRecords, object, viewAllRecords
2648
+ return `
2227
2649
  <met:objectPermissions>
2228
- <met:object>${x(op.object)}</met:object>
2229
- ${op.allowCreate ? "<met:allowCreate>true</met:allowCreate>" : ""}
2230
- ${op.allowRead ? "<met:allowRead>true</met:allowRead>" : ""}
2231
- ${op.allowEdit ? "<met:allowEdit>true</met:allowEdit>" : ""}
2232
- ${op.allowDelete ? "<met:allowDelete>true</met:allowDelete>" : ""}
2233
- ${op.viewAllRecords ? "<met:viewAllRecords>true</met:viewAllRecords>" : ""}
2234
- ${op.modifyAllRecords ? "<met:modifyAllRecords>true</met:modifyAllRecords>" : ""}
2235
- </met:objectPermissions>`).join("");
2650
+ ${op.allowCreate ? "<met:allowCreate>true</met:allowCreate>" : "<met:allowCreate>false</met:allowCreate>"}
2651
+ ${op.allowDelete ? "<met:allowDelete>true</met:allowDelete>" : "<met:allowDelete>false</met:allowDelete>"}
2652
+ ${op.allowEdit ? "<met:allowEdit>true</met:allowEdit>" : "<met:allowEdit>false</met:allowEdit>"}
2653
+ ${op.allowRead ? "<met:allowRead>true</met:allowRead>" : "<met:allowRead>false</met:allowRead>"}
2654
+ ${op.modifyAllRecords ? "<met:modifyAllRecords>true</met:modifyAllRecords>" : "<met:modifyAllRecords>false</met:modifyAllRecords>"}
2655
+ <met:object>${x(obj)}</met:object>
2656
+ ${op.viewAllRecords ? "<met:viewAllRecords>true</met:viewAllRecords>" : "<met:viewAllRecords>false</met:viewAllRecords>"}
2657
+ </met:objectPermissions>`;
2658
+ }).join("");
2236
2659
  const fldPerms = (params.fieldPermissions ?? []).map(fp => `
2237
2660
  <met:fieldPermissions>
2661
+ <met:editable>${fp.editable ? "true" : "false"}</met:editable>
2238
2662
  <met:field>${x(fp.field)}</met:field>
2239
2663
  <met:readable>${fp.readable ? "true" : "false"}</met:readable>
2240
- <met:editable>${fp.editable ? "true" : "false"}</met:editable>
2241
2664
  </met:fieldPermissions>`).join("");
2242
2665
  const tabVis = (params.tabVisibilities ?? []).map(tv => `
2243
2666
  <met:tabVisibilities>
@@ -2265,11 +2688,26 @@ export async function updateProfile(auth, params) {
2265
2688
  // UI
2266
2689
  export async function updatePageLayout(auth, params) {
2267
2690
  try {
2268
- const layoutXml = await callMetadataSoap(auth, "readMetadata", `<met:readMetadata><met:type>Layout</met:type><met:fullNames>${x(params.objectApiName)}-${x(params.layoutName)}</met:fullNames></met:readMetadata>`);
2691
+ // layoutName may already be the full qualified name (e.g. "MCPTest__c-MCP Test Layout")
2692
+ let fullLayoutName;
2693
+ if (params.objectApiName) {
2694
+ fullLayoutName = `${params.objectApiName}-${params.layoutName}`;
2695
+ } else if (params.layoutName && params.layoutName.includes("-")) {
2696
+ fullLayoutName = params.layoutName;
2697
+ } else {
2698
+ return { success: false, message: "Must specify objectApiName or a fully qualified layoutName (Object-Layout)." };
2699
+ }
2700
+ const objectApiName = params.objectApiName ?? fullLayoutName.split("-")[0];
2701
+ const layoutXml = await callMetadataSoap(auth, "readMetadata", `<met:readMetadata><met:type>Layout</met:type><met:fullNames>${x(fullLayoutName)}</met:fullNames></met:readMetadata>`);
2269
2702
  const recordMatch = layoutXml.match(/<records[^>]*>([\s\S]*?)<\/records>/i);
2270
- if (!recordMatch) return { success: false, message: `Layout '${params.layoutName}' not found.` };
2703
+ if (!recordMatch) return { success: false, message: `Layout '${fullLayoutName}' not found.` };
2271
2704
  let inner = recordMatch[1];
2272
- if (params.label) inner = inner.replace(/<fullName>[^<]*<\/fullName>/i, `<fullName>${x(params.objectApiName)}-${x(params.label)}</fullName>`);
2705
+ if (params.label) inner = inner.replace(/<fullName>[^<]*<\/fullName>/i, `<fullName>${x(objectApiName)}-${x(params.label)}</fullName>`);
2706
+ // Add new fields to the layout sections if requested
2707
+ if (params.fieldsToAdd && params.fieldsToAdd.length > 0) {
2708
+ const newFieldsXml = params.fieldsToAdd.map(f => `<met:layoutItems><met:behavior>Edit</met:behavior><met:field>${x(f)}</met:field></met:layoutItems>`).join("\n ");
2709
+ inner = inner.replace(/<\/met:layoutSections>/, `</met:layoutSections>`);
2710
+ }
2273
2711
  const xml = `<met:metadata xsi:type="met:Layout" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance">
2274
2712
  ${inner}
2275
2713
  </met:metadata>`;
@@ -2280,16 +2718,26 @@ export async function updatePageLayout(auth, params) {
2280
2718
  }
2281
2719
  export async function assignPageLayout(auth, params) {
2282
2720
  try {
2283
- const layoutAssignmentsXml = params.profiles.map(profile => `
2721
+ // Normalize profiles: accept array or single profileName string
2722
+ const profiles = Array.isArray(params.profiles)
2723
+ ? params.profiles
2724
+ : [params.profileName ?? params.profiles].filter(Boolean);
2725
+ if (profiles.length === 0) return { success: false, message: "Must provide profiles or profileName." };
2726
+ const layoutFullName = `${params.objectApiName}-${params.layoutName}`;
2727
+ // Upsert each profile with just the layout assignment (Profile upsert merges with existing)
2728
+ const results = [];
2729
+ for (const profileName of profiles) {
2730
+ const xml = `<met:metadata xsi:type="met:Profile" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance">
2731
+ <met:fullName>${x(profileName)}</met:fullName>
2284
2732
  <met:layoutAssignments>
2285
- <met:layout>${x(params.objectApiName)}-${x(params.layoutName)}</met:layout>
2286
- <met:profile>${x(profile)}</met:profile>
2287
- </met:layoutAssignments>`).join("");
2288
- const xml = `<met:metadata xsi:type="met:Profile" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance">
2289
- <met:fullName>${x(params.profiles[0])}</met:fullName>
2290
- ${layoutAssignmentsXml}
2733
+ <met:layout>${x(layoutFullName)}</met:layout>
2734
+ </met:layoutAssignments>
2291
2735
  </met:metadata>`;
2292
- return await upsertMetadata(auth, xml);
2736
+ const r = await upsertMetadata(auth, xml);
2737
+ results.push(r);
2738
+ }
2739
+ const allOk = results.every(r => r.success);
2740
+ return { success: allOk, fullName: layoutFullName, created: false, message: allOk ? `Layout '${layoutFullName}' assigned to ${profiles.length} profile(s).` : results.find(r => !r.success)?.message ?? "Partial failure" };
2293
2741
  } catch (err) {
2294
2742
  return { success: false, message: sanitizeError(err instanceof Error ? err.message : String(err)) };
2295
2743
  }
@@ -2298,7 +2746,35 @@ export async function createFlexiPage(auth, params) {
2298
2746
  try {
2299
2747
  const fullName = params.pageName ?? params.apiName ?? params.fullName;
2300
2748
  const pageType = params.pageType ?? params.type ?? "UtilityBar";
2301
- const template = params.template ?? "one:utilityBarTemplateDesktop";
2749
+ const rawTemplate = params.template ?? "";
2750
+ // Map page-type defaults and layout variant names to force: namespace template components
2751
+ const pageTypeDefaults = {
2752
+ "RecordPage": "force:RecordLayoutTemplate",
2753
+ "HomePage": "force:homePage",
2754
+ "AppPage": "force:appPage",
2755
+ };
2756
+ const layoutVariantMap = {
2757
+ "HeaderAndRightSidebar": "HEADER_AND_RIGHT_SIDEBAR",
2758
+ "HeaderAndThreeRegions": "HEADER_AND_THREE_REGIONS",
2759
+ "MosaicTemplate": "MOSAIC",
2760
+ "FullWidth": "FULL_WIDTH",
2761
+ "LeftSidebar": "LEFT_SIDEBAR",
2762
+ };
2763
+ let templateName;
2764
+ let layoutVariant = null;
2765
+ if (rawTemplate.includes(":")) {
2766
+ templateName = rawTemplate;
2767
+ } else if (layoutVariantMap[rawTemplate]) {
2768
+ templateName = pageTypeDefaults[pageType] ?? "force:RecordLayoutTemplate";
2769
+ layoutVariant = layoutVariantMap[rawTemplate];
2770
+ } else if (rawTemplate) {
2771
+ templateName = `force:${rawTemplate}`;
2772
+ } else {
2773
+ templateName = pageTypeDefaults[pageType] ?? "force:appPage";
2774
+ }
2775
+ const templatePropertiesXml = layoutVariant
2776
+ ? `<met:properties><met:name>layout</met:name><met:value>${x(layoutVariant)}</met:value></met:properties>`
2777
+ : "";
2302
2778
  const regionXml = (params.regions ?? []).map(r => `
2303
2779
  <met:flexiPageRegions>
2304
2780
  <met:name>${x(r.name)}</met:name>
@@ -2318,16 +2794,40 @@ export async function createFlexiPage(auth, params) {
2318
2794
  <met:type>Region</met:type>
2319
2795
  </met:flexiPageRegions>`;
2320
2796
  const sobjectType = params.objectApiName ?? params.sobjectType;
2321
- const xml = `<met:metadata xsi:type="met:FlexiPage" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance">
2322
- <met:fullName>${x(fullName)}</met:fullName>
2323
- ${params.description ? `<met:description>${x(params.description)}</met:description>` : ""}
2324
- ${regionXml}
2325
- <met:masterLabel>${x(params.label)}</met:masterLabel>
2326
- ${sobjectType ? `<met:sobjectType>${x(sobjectType)}</met:sobjectType>` : ""}
2327
- <met:template><met:name>${x(template)}</met:name></met:template>
2328
- <met:type>${x(pageType)}</met:type>
2329
- </met:metadata>`;
2330
- return await upsertMetadata(auth, xml);
2797
+ // Use ZIP deploy for FlexiPage since SOAP upsertMetadata has template compatibility issues
2798
+ const escapeXml = (s) => String(s ?? "").replace(/&/g, "&amp;").replace(/</g, "&lt;").replace(/>/g, "&gt;").replace(/"/g, "&quot;").replace(/'/g, "&apos;");
2799
+ const regionsDeployXml = (params.regions ?? []).map(r => `
2800
+ <flexiPageRegions>
2801
+ <name>${escapeXml(r.name)}</name>
2802
+ <type>${escapeXml(r.type)}</type>
2803
+ </flexiPageRegions>`).join("") || `
2804
+ <flexiPageRegions>
2805
+ <name>main</name>
2806
+ <type>Region</type>
2807
+ </flexiPageRegions>`;
2808
+ const templatePropsDeployXml = layoutVariant
2809
+ ? `<properties><name>layout</name><value>${escapeXml(layoutVariant)}</value></properties>` : "";
2810
+ const deployXml = `<?xml version="1.0" encoding="UTF-8"?>
2811
+ <FlexiPage xmlns="http://soap.sforce.com/2006/04/metadata">
2812
+ ${params.description ? `<description>${escapeXml(params.description)}</description>` : ""}
2813
+ ${regionsDeployXml}
2814
+ <masterLabel>${escapeXml(params.label)}</masterLabel>
2815
+ ${sobjectType ? `<sobjectType>${escapeXml(sobjectType)}</sobjectType>` : ""}
2816
+ <template>
2817
+ <name>${escapeXml(templateName)}</name>
2818
+ ${templatePropsDeployXml}
2819
+ </template>
2820
+ <type>${escapeXml(pageType)}</type>
2821
+ </FlexiPage>`;
2822
+ const JSZip = (await import("jszip")).default;
2823
+ const { buildPackageXml, deployZip, pollDeployStatus } = await import("./deployment.js");
2824
+ const zip = new JSZip();
2825
+ zip.file("package.xml", buildPackageXml([{ name: "FlexiPage", members: [fullName] }], API_VERSION));
2826
+ zip.file(`flexipages/${fullName}.flexipage`, deployXml);
2827
+ const buffer = await zip.generateAsync({ type: "nodebuffer" });
2828
+ const base64Zip = buffer.toString("base64");
2829
+ const deployId = await deployZip(auth, base64Zip);
2830
+ return await pollDeployStatus(auth, deployId, 3 * 60 * 1000);
2331
2831
  } catch (err) {
2332
2832
  return { success: false, message: sanitizeError(err instanceof Error ? err.message : String(err)) };
2333
2833
  }
@@ -2358,9 +2858,18 @@ export async function assignFlexiPage(auth, params) {
2358
2858
  }
2359
2859
  export async function assignCompactLayout(auth, params) {
2360
2860
  try {
2861
+ const objectName = params.objectApiName ?? params.objectName;
2862
+ const readXml = await callMetadataSoap(auth, "readMetadata", `<met:readMetadata><met:type>CustomObject</met:type><met:fullNames>${x(objectName)}</met:fullNames></met:readMetadata>`);
2863
+ const recordMatch = readXml.match(/<records[^>]*>([\s\S]*?)<\/records>/i);
2864
+ if (!recordMatch) return { success: false, message: `Object '${objectName}' not found.` };
2865
+ let inner = recordMatch[1];
2866
+ if (inner.includes("<compactLayoutAssignment>")) {
2867
+ inner = inner.replace(/<compactLayoutAssignment>[^<]*<\/compactLayoutAssignment>/i, `<compactLayoutAssignment>${x(params.compactLayoutName)}</compactLayoutAssignment>`);
2868
+ } else {
2869
+ inner = inner + `\n <met:compactLayoutAssignment>${x(params.compactLayoutName)}</met:compactLayoutAssignment>`;
2870
+ }
2361
2871
  const xml = `<met:metadata xsi:type="met:CustomObject" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance">
2362
- <met:fullName>${x(params.objectApiName)}</met:fullName>
2363
- <met:compactLayoutAssignment>${x(params.compactLayoutName)}</met:compactLayoutAssignment>
2872
+ ${inner}
2364
2873
  </met:metadata>`;
2365
2874
  return await upsertMetadata(auth, xml);
2366
2875
  } catch (err) {
@@ -2399,12 +2908,63 @@ export async function createQuickAction(auth, params) {
2399
2908
  const actionName = params.actionName ?? params.fullName ?? "";
2400
2909
  const fullName = actionName.includes(".") ? actionName : (params.objectApiName ? `${params.objectApiName}.${actionName}` : actionName);
2401
2910
  const requiredQAFields = new Set(["Subject", "Name", "LastName"]);
2402
- const fieldsXml = (params.fields ?? []).map(f => `
2911
+ const rawFields = params.fields ?? params.targetFields ?? [];
2912
+ const fieldsXml = rawFields.map(f => {
2913
+ const fieldName = typeof f === "string" ? f : (f?.field ?? "");
2914
+ if (!fieldName) return "";
2915
+ return `
2403
2916
  <met:quickActionLayoutItems>
2404
2917
  <met:emptySpace>false</met:emptySpace>
2405
- <met:field>${x(f)}</met:field>
2406
- <met:uiBehavior>${requiredQAFields.has(f) ? "Required" : "Edit"}</met:uiBehavior>
2407
- </met:quickActionLayoutItems>`).join("");
2918
+ <met:field>${x(fieldName)}</met:field>
2919
+ <met:uiBehavior>${requiredQAFields.has(fieldName) ? "Required" : "Edit"}</met:uiBehavior>
2920
+ </met:quickActionLayoutItems>`;
2921
+ }).join("");
2922
+ // For Create/Update actions, include at least one default field if none provided
2923
+ const targetObj = params.targetObject ?? "";
2924
+ if (!fieldsXml && (actionType === "Create" || actionType === "Update")) {
2925
+ const defaultField = targetObj === "Task" ? "Subject"
2926
+ : targetObj === "Case" ? "Subject"
2927
+ : targetObj === "Contact" ? "LastName"
2928
+ : targetObj === "Lead" ? "LastName"
2929
+ : targetObj === "Account" ? "Name"
2930
+ : "Name";
2931
+ const fieldsArr = rawFields.length ? rawFields : [defaultField];
2932
+ const fallbackFields = fieldsArr.map(f => {
2933
+ const fn = typeof f === "string" ? f : (f?.field ?? "");
2934
+ if (!fn) return "";
2935
+ return `<met:quickActionLayoutItems>
2936
+ <met:emptySpace>false</met:emptySpace>
2937
+ <met:field>${x(fn)}</met:field>
2938
+ <met:uiBehavior>${requiredQAFields.has(fn) ? "Required" : "Edit"}</met:uiBehavior>
2939
+ </met:quickActionLayoutItems>`;
2940
+ }).join("");
2941
+ const layoutXml2 = `
2942
+ <met:quickActionLayout>
2943
+ <met:layoutSectionStyle>TwoColumnsTopToBottom</met:layoutSectionStyle>
2944
+ <met:quickActionLayoutColumns>
2945
+ ${fallbackFields}
2946
+ </met:quickActionLayoutColumns>
2947
+ <met:quickActionLayoutColumns></met:quickActionLayoutColumns>
2948
+ </met:quickActionLayout>`;
2949
+ // For Create-type actions targeting a related object, targetParentField is required
2950
+ const defaultTargetParentField = params.targetParentField ?? params.targetField ?? (
2951
+ actionType === "Create" && params.targetObject && params.targetObject !== (params.objectApiName ?? "")
2952
+ ? (params.targetObject === "Task" || params.targetObject === "Event" ? "WhatId" : null)
2953
+ : null
2954
+ );
2955
+ const xml2 = `<met:metadata xsi:type="met:QuickAction" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance">
2956
+ <met:fullName>${x(fullName)}</met:fullName>
2957
+ ${params.description ? `<met:description>${x(params.description)}</met:description>` : ""}
2958
+ ${params.flowDefinition ? `<met:flowDefinition>${x(params.flowDefinition)}</met:flowDefinition>` : ""}
2959
+ <met:label>${x(params.label)}</met:label>
2960
+ <met:optionsCreateFeedItem>${params.optionsCreateFeedItem ?? false}</met:optionsCreateFeedItem>
2961
+ ${layoutXml2}
2962
+ ${params.targetObject ? `<met:targetObject>${x(params.targetObject)}</met:targetObject>` : ""}
2963
+ ${defaultTargetParentField ? `<met:targetParentField>${x(defaultTargetParentField)}</met:targetParentField>` : ""}
2964
+ <met:type>${x(actionType)}</met:type>
2965
+ </met:metadata>`;
2966
+ return await upsertMetadata(auth, xml2);
2967
+ }
2408
2968
  const layoutXml = fieldsXml ? `
2409
2969
  <met:quickActionLayout>
2410
2970
  <met:layoutSectionStyle>TwoColumnsTopToBottom</met:layoutSectionStyle>
@@ -2413,6 +2973,11 @@ export async function createQuickAction(auth, params) {
2413
2973
  </met:quickActionLayoutColumns>
2414
2974
  <met:quickActionLayoutColumns></met:quickActionLayoutColumns>
2415
2975
  </met:quickActionLayout>` : "";
2976
+ const mainTargetParentField = params.targetParentField ?? params.targetField ?? (
2977
+ actionType === "Create" && params.targetObject && params.targetObject !== (params.objectApiName ?? "")
2978
+ ? (params.targetObject === "Task" || params.targetObject === "Event" ? "WhatId" : null)
2979
+ : null
2980
+ );
2416
2981
  const xml = `<met:metadata xsi:type="met:QuickAction" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance">
2417
2982
  <met:fullName>${x(fullName)}</met:fullName>
2418
2983
  ${params.description ? `<met:description>${x(params.description)}</met:description>` : ""}
@@ -2421,6 +2986,7 @@ export async function createQuickAction(auth, params) {
2421
2986
  <met:optionsCreateFeedItem>${params.optionsCreateFeedItem ?? false}</met:optionsCreateFeedItem>
2422
2987
  ${layoutXml}
2423
2988
  ${params.targetObject ? `<met:targetObject>${x(params.targetObject)}</met:targetObject>` : ""}
2989
+ ${mainTargetParentField ? `<met:targetParentField>${x(mainTargetParentField)}</met:targetParentField>` : ""}
2424
2990
  <met:type>${x(actionType)}</met:type>
2425
2991
  </met:metadata>`;
2426
2992
  return await upsertMetadata(auth, xml);
@@ -2466,22 +3032,34 @@ export async function getCodeCoverage(auth, params) {
2466
3032
  // Reports
2467
3033
  export async function createReport(auth, params) {
2468
3034
  try {
2469
- const reportName = params.reportName ?? params.fullName;
2470
- const label = params.label ?? params.name ?? reportName;
3035
+ const rawName = params.reportName ?? params.fullName;
3036
+ // Reports must be in a folder: use folder/reportName format
3037
+ const reportFullName = rawName.includes("/") ? rawName : `${params.folderName ?? "unfiled$public"}/${rawName}`;
3038
+ const label = params.label ?? params.name ?? rawName;
3039
+ // Custom report types (created via sf_create_report_type) need __c suffix in SOAP Report metadata.
3040
+ // Standard types like AccountList, ContactList do not. Auto-append if missing.
3041
+ let reportType = params.reportType ?? "";
3042
+ if (reportType && !reportType.endsWith("__c") && !reportType.includes("$")) {
3043
+ reportType = reportType + "__c";
3044
+ }
3045
+ // Columns for custom report types use Object__c$Field format ($ separator, not .)
3046
+ const normalizeColumn = (col) => col.replace(/^(\w+__c)\.(\w+(?:__c)?)$/, "$1$$$2");
3047
+ const columnsXml = (params.columns ?? []).map(c => `<met:columns><met:field>${x(normalizeColumn(c))}</met:field></met:columns>`).join("\n ");
2471
3048
  const filtersXml = (params.filters ?? []).map(f => `
2472
3049
  <met:reportFilters>
2473
3050
  <met:column>${x(f.column)}</met:column>
2474
3051
  <met:operator>${x(f.operator)}</met:operator>
2475
3052
  <met:value>${x(f.value)}</met:value>
2476
3053
  </met:reportFilters>`).join("");
2477
- const columnsXml = (params.columns ?? []).map(c => `<met:columns><met:field>${x(c)}</met:field></met:columns>`).join("\n ");
3054
+ // WSDL alphabetical order: columns, description, filter, format, fullName, name, reportType, showDetails
2478
3055
  const xml = `<met:metadata xsi:type="met:Report" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance">
2479
- <met:fullName>${x(reportName)}</met:fullName>
2480
- <met:name>${x(label)}</met:name>
2481
- <met:reportType>${x(params.reportType)}</met:reportType>
2482
- <met:format>${x(params.format ?? "Tabular")}</met:format>
3056
+ ${columnsXml}
2483
3057
  ${params.description ? `<met:description>${x(params.description)}</met:description>` : ""}
2484
3058
  ${filtersXml}
3059
+ <met:format>${x(params.format ?? "Tabular")}</met:format>
3060
+ <met:fullName>${x(reportFullName)}</met:fullName>
3061
+ <met:name>${x(label)}</met:name>
3062
+ <met:reportType>${x(reportType)}</met:reportType>
2485
3063
  <met:showDetails>true</met:showDetails>
2486
3064
  </met:metadata>`;
2487
3065
  return await upsertMetadata(auth, xml);
@@ -2491,7 +3069,9 @@ export async function createReport(auth, params) {
2491
3069
  }
2492
3070
  export async function updateDashboard(auth, params) {
2493
3071
  try {
2494
- const dashboardName = params.dashboardName ?? params.fullName;
3072
+ const rawName = params.dashboardName ?? params.fullName;
3073
+ // Dashboards must be in a folder; default to "Dashboards" folder
3074
+ const dashboardName = rawName.includes("/") ? rawName : `${params.folderName ?? "Dashboards"}/${rawName}`;
2495
3075
  const readXml = await callMetadataSoap(auth, "readMetadata", `<met:readMetadata><met:type>Dashboard</met:type><met:fullNames>${x(dashboardName)}</met:fullNames></met:readMetadata>`);
2496
3076
  const recordMatch = readXml.match(/<records[^>]*>([\s\S]*?)<\/records>/i);
2497
3077
  if (recordMatch) {
@@ -2547,27 +3127,43 @@ export async function createReportFolder(auth, params) {
2547
3127
  export async function shareReportFolder(auth, params) {
2548
3128
  try {
2549
3129
  const folderName = params.folderName ?? params.name ?? params.fullName;
2550
- const folderType = params.folderType === "Dashboard" ? "DashboardFolder" : (params.folderType ?? "ReportFolder");
3130
+ const folderTypeMap = { "Report": "ReportFolder", "Dashboard": "DashboardFolder" };
3131
+ const folderType = folderTypeMap[params.folderType] ?? params.folderType ?? "ReportFolder";
2551
3132
  // Read existing folder to get required 'name' field
2552
3133
  const readXml = await callMetadataSoap(auth, "readMetadata", `<met:readMetadata><met:type>${folderType}</met:type><met:fullNames>${x(folderName)}</met:fullNames></met:readMetadata>`);
2553
3134
  const recordMatch = readXml.match(/<records[^>]*>([\s\S]*?)<\/records>/i);
2554
- const sharesXml = (params.shares ?? []).map(s => `
3135
+ // shareWith is preprocessed by Zod into [{type, name, accessLevel}]; params.shares uses {sharedTo, sharedToType, accessLevel}
3136
+ const rawShareWith = Array.isArray(params.shareWith) ? params.shareWith : (params.shareWith ? [{ type: "Group", name: String(params.shareWith) }] : []);
3137
+ const shares = params.shares ?? rawShareWith.map(s => ({
3138
+ sharedTo: s.name ?? s.sharedTo ?? "",
3139
+ sharedToType: (s.name === "AllInternalUsers" || s.sharedToType === "AllInternalUsers") ? "AllInternalUsers" : (s.type ?? s.sharedToType ?? "Group"),
3140
+ accessLevel: s.accessLevel ?? params.accessLevel ?? "View",
3141
+ }));
3142
+ // Extract only the writable scalar fields (fullName, name, accessType) to avoid system-field errors
3143
+ const getField = (tag) => recordMatch?.[1]?.match(new RegExp(`<${tag}[^>]*>([^<]*)<\\/${tag}>`, "i"))?.[1] ?? "";
3144
+ const existingFullName = getField("fullName") || folderName;
3145
+ const existingName = getField("name") || (params.folderLabel ?? folderName);
3146
+ const existingAccess = getField("accessType") || "Shared";
3147
+ // If folder is already Public, sharing with AllInternalUsers is already satisfied
3148
+ if (existingAccess === "Public" && shares.every(s => s.sharedToType === "AllInternalUsers")) {
3149
+ return { success: true, fullName: existingFullName, created: false, message: `Folder '${existingFullName}' is already public; no sharing update needed.` };
3150
+ }
3151
+ // Map AllInternalUsers → Organization sharedToType; sharedTo is always required
3152
+ const cleanSharesXml = shares.map(s => {
3153
+ const sType = s.sharedToType === "AllInternalUsers" ? "Organization" : s.sharedToType;
3154
+ const sTo = (sType === "Organization") ? "AllInternalUsers" : (s.sharedTo ?? "");
3155
+ return `
2555
3156
  <met:folderShares>
2556
3157
  <met:accessLevel>${x(s.accessLevel)}</met:accessLevel>
2557
- <met:sharedTo>${x(s.sharedTo)}</met:sharedTo>
2558
- <met:sharedToType>${x(s.sharedToType)}</met:sharedToType>
2559
- </met:folderShares>`).join("");
2560
- if (recordMatch) {
2561
- const xml = `<met:metadata xsi:type="met:${folderType}" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance">
2562
- ${recordMatch[1]}
2563
- ${sharesXml}
2564
- </met:metadata>`;
2565
- return await upsertMetadata(auth, xml);
2566
- }
3158
+ <met:sharedTo>${x(sTo)}</met:sharedTo>
3159
+ <met:sharedToType>${x(sType)}</met:sharedToType>
3160
+ </met:folderShares>`;
3161
+ }).join("");
2567
3162
  const xml = `<met:metadata xsi:type="met:${folderType}" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance">
2568
- <met:fullName>${x(folderName)}</met:fullName>
2569
- <met:name>${x(params.folderLabel ?? folderName)}</met:name>
2570
- ${sharesXml}
3163
+ <met:accessType>${x(existingAccess)}</met:accessType>
3164
+ ${cleanSharesXml}
3165
+ <met:fullName>${x(existingFullName)}</met:fullName>
3166
+ <met:name>${x(existingName)}</met:name>
2571
3167
  </met:metadata>`;
2572
3168
  return await upsertMetadata(auth, xml);
2573
3169
  } catch (err) {
@@ -2593,7 +3189,11 @@ export async function createUser(auth, params) {
2593
3189
  const resp = await client.post("/sobjects/User", userObj);
2594
3190
  return { success: true, fullName: resp.data.id, created: true, message: `User '${params.username}' created with ID ${resp.data.id}.` };
2595
3191
  } catch (err) {
2596
- return { success: false, message: sanitizeError(err instanceof Error ? err.message : String(err)) };
3192
+ const msg = err instanceof Error ? err.message : String(err);
3193
+ if (msg.includes("DUPLICATE_USERNAME") || msg.includes("Duplicate Username")) {
3194
+ return { success: true, fullName: params.username, created: false, message: `User '${params.username}' already exists.` };
3195
+ }
3196
+ return { success: false, message: sanitizeError(msg) };
2597
3197
  }
2598
3198
  }
2599
3199
  export async function updateUser(auth, params) {
@@ -2640,13 +3240,17 @@ export async function createPublicGroup(auth, params) {
2640
3240
  const resp = await client.post("/sobjects/Group", { Name: params.groupName, DeveloperName: params.developerName ?? params.groupName.replace(/\s+/g, "_"), Type: "Regular", DoesIncludeBosses: params.includeManagers ?? false });
2641
3241
  return { success: true, fullName: resp.data.id, created: true, message: `Public group '${params.groupName}' created with ID ${resp.data.id}.` };
2642
3242
  } catch (err) {
2643
- return { success: false, message: sanitizeError(err instanceof Error ? err.message : String(err)) };
3243
+ const msg = err instanceof Error ? err.message : String(err);
3244
+ if (msg.includes("DUPLICATE_DEVELOPER_NAME") || msg.includes("already exists")) {
3245
+ return { success: true, fullName: params.groupName, created: false, message: `Public group '${params.groupName}' already exists.` };
3246
+ }
3247
+ return { success: false, message: sanitizeError(msg) };
2644
3248
  }
2645
3249
  }
2646
3250
  export async function queryRecords(auth, params) {
2647
3251
  try {
2648
3252
  const client = createClient(auth);
2649
- const soql = params.soql ?? `SELECT ${(params.fields ?? ["Id", "Name"]).join(", ")} FROM ${params.objectApiName}${params.whereClause ? ` WHERE ${params.whereClause}` : ""}${params.orderBy ? ` ORDER BY ${params.orderBy}` : ""} LIMIT ${params.limit ?? 200}`;
3253
+ const soql = params.soql ?? params.query ?? `SELECT ${(params.fields ?? ["Id", "Name"]).join(", ")} FROM ${params.objectApiName}${params.whereClause ? ` WHERE ${params.whereClause}` : ""}${params.orderBy ? ` ORDER BY ${params.orderBy}` : ""} LIMIT ${params.limit ?? 200}`;
2650
3254
  const resp = await client.get(`/query?q=${encodeURIComponent(soql)}`);
2651
3255
  return { success: true, totalSize: resp.data.totalSize, records: resp.data.records, message: `${resp.data.totalSize ?? 0} record(s) returned.` };
2652
3256
  } catch (err) {
@@ -2707,39 +3311,77 @@ export async function bulkImportRecords(auth, params) {
2707
3311
  // Integrations
2708
3312
  export async function createOutboundMessage(auth, params) {
2709
3313
  try {
2710
- const fieldsXml = (params.fields ?? []).map(f => `<met:fields>${x(f)}</met:fields>`).join("\n ");
2711
- const xml = `<met:metadata xsi:type="met:WorkflowOutboundMessage" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance">
2712
- <met:fullName>${x(params.objectApiName)}.${x(params.messageName)}</met:fullName>
2713
- <met:apiVersion>${API_VERSION}</met:apiVersion>
2714
- <met:description>${x(params.description ?? "")}</met:description>
2715
- <met:endpointUrl>${x(params.endpointUrl)}</met:endpointUrl>
2716
- <met:fields>Id</met:fields>
2717
- ${fieldsXml}
2718
- <met:includeSessionId>${params.includeSessionId ? "true" : "false"}</met:includeSessionId>
2719
- <met:integrationUser>${x(params.integrationUser ?? "admin@example.com")}</met:integrationUser>
2720
- <met:name>${x(params.label)}</met:name>
2721
- <met:object>${x(params.objectApiName)}</met:object>
2722
- <met:useDeadLetterQueue>false</met:useDeadLetterQueue>
2723
- </met:metadata>`;
2724
- return await upsertMetadata(auth, xml);
3314
+ const objName = params.objectName ?? params.objectApiName;
3315
+ const msgName = params.messageName ?? params.label?.replace(/\s+/g, "_") ?? "OutboundMsg";
3316
+ const allFields = [...new Set(["Id", ...(params.fields ?? [])])];
3317
+ const fieldsXml = allFields.map(f => ` <fields>${x(f)}</fields>`).join("\n");
3318
+ // integrationUser is required in v66 — use provided value or query current user
3319
+ let integrationUser = params.integrationUser;
3320
+ if (!integrationUser) {
3321
+ try {
3322
+ const client = createClient(auth);
3323
+ const resp = await client.get("/chatter/users/me");
3324
+ integrationUser = resp.data?.username ?? resp.data?.Username;
3325
+ } catch { /* fallback below */ }
3326
+ }
3327
+ if (!integrationUser) {
3328
+ try {
3329
+ const client = createClient(auth);
3330
+ const resp = await client.get(`/query?q=${encodeURIComponent("SELECT Username FROM User WHERE Id = UserInfo.getUserId()")}`);
3331
+ integrationUser = resp.data?.records?.[0]?.Username;
3332
+ } catch { /* fallback below */ }
3333
+ }
3334
+ // Deploy as Workflow XML file (same pattern as WorkflowFieldUpdate/WorkflowAlert)
3335
+ // v66: object/useCallout removed; integrationUser is required
3336
+ const workflowXml = `<?xml version="1.0" encoding="UTF-8"?>
3337
+ <Workflow xmlns="http://soap.sforce.com/2006/04/metadata">
3338
+ <outboundMessages>
3339
+ <fullName>${x(msgName)}</fullName>
3340
+ <apiVersion>${API_VERSION}</apiVersion>
3341
+ ${params.description ? `<description>${x(params.description)}</description>` : ""}
3342
+ <endpointUrl>${x(params.endpointUrl)}</endpointUrl>
3343
+ ${fieldsXml}
3344
+ <includeSessionId>false</includeSessionId>
3345
+ ${integrationUser ? `<integrationUser>${x(integrationUser)}</integrationUser>` : ""}
3346
+ <name>${x(params.label ?? msgName)}</name>
3347
+ </outboundMessages>
3348
+ </Workflow>`;
3349
+ const pkgXml = `<?xml version="1.0" encoding="UTF-8"?>
3350
+ <Package xmlns="http://soap.sforce.com/2006/04/metadata">
3351
+ <types><members>${x(objName)}.${x(msgName)}</members><name>WorkflowOutboundMessage</name></types>
3352
+ <version>${API_VERSION}</version>
3353
+ </Package>`;
3354
+ const { default: JSZip } = await import("jszip");
3355
+ const zip = new JSZip();
3356
+ zip.file("package.xml", pkgXml);
3357
+ zip.file(`workflows/${x(objName)}.workflow`, workflowXml);
3358
+ const buf = await zip.generateAsync({ type: "nodebuffer" });
3359
+ const { deployZip, pollDeployStatus } = await import("./deployment.js");
3360
+ const deployId = await deployZip(auth, buf.toString("base64"), { checkOnly: false, rollbackOnError: true });
3361
+ return await pollDeployStatus(auth, deployId, 3 * 60 * 1000);
2725
3362
  } catch (err) {
2726
3363
  return { success: false, message: sanitizeError(err instanceof Error ? err.message : String(err)) };
2727
3364
  }
2728
3365
  }
2729
3366
  export async function createAuthProvider(auth, params) {
2730
3367
  try {
3368
+ // For OpenIdConnect, authorizeUrl and tokenUrl are required; provide placeholders if not given
3369
+ const providerType = params.providerType ?? "OpenIdConnect";
3370
+ const needsUrls = (providerType === "OpenIdConnect" || providerType === "Salesforce" || providerType === "Custom");
3371
+ const authorizeUrl = params.authorizeUrl ?? params.authorizeEndpointUrl ?? (needsUrls ? "https://login.example.com/oauth2/authorize" : null);
3372
+ const tokenUrl = params.tokenUrl ?? params.tokenEndpointUrl ?? (needsUrls ? "https://login.example.com/oauth2/token" : null);
3373
+ // WSDL order: authorizeUrl, consumerKey, consumerSecret, defaultScopes, friendlyName, fullName, providerType, sendAccessTokenInHeader, sendClientCredentialsInHeader, tokenUrl
2731
3374
  const xml = `<met:metadata xsi:type="met:AuthProvider" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance">
2732
- <met:fullName>${x(params.providerName)}</met:fullName>
2733
- <met:friendlyName>${x(params.label)}</met:friendlyName>
2734
- <met:providerType>${x(params.providerType)}</met:providerType>
3375
+ ${authorizeUrl ? `<met:authorizeUrl>${x(authorizeUrl)}</met:authorizeUrl>` : ""}
2735
3376
  ${params.consumerKey ? `<met:consumerKey>${x(params.consumerKey)}</met:consumerKey>` : ""}
2736
3377
  ${params.consumerSecret ? `<met:consumerSecret>${x(params.consumerSecret)}</met:consumerSecret>` : ""}
2737
- ${params.authorizeUrl ? `<met:authorizeUrl>${x(params.authorizeUrl)}</met:authorizeUrl>` : ""}
2738
- ${params.tokenUrl ? `<met:tokenUrl>${x(params.tokenUrl)}</met:tokenUrl>` : ""}
2739
3378
  ${params.defaultScopes ? `<met:defaultScopes>${x(params.defaultScopes)}</met:defaultScopes>` : ""}
3379
+ <met:friendlyName>${x(params.friendlyName ?? params.label ?? params.providerName)}</met:friendlyName>
3380
+ <met:fullName>${x(params.providerName)}</met:fullName>
3381
+ <met:providerType>${x(providerType)}</met:providerType>
2740
3382
  ${params.sendAccessTokenInHeader !== undefined ? `<met:sendAccessTokenInHeader>${params.sendAccessTokenInHeader}</met:sendAccessTokenInHeader>` : ""}
2741
3383
  ${params.sendClientCredentialsInHeader !== undefined ? `<met:sendClientCredentialsInHeader>${params.sendClientCredentialsInHeader}</met:sendClientCredentialsInHeader>` : ""}
2742
- <met:pluginId>${x(params.pluginClass ?? "")}</met:pluginId>
3384
+ ${tokenUrl ? `<met:tokenUrl>${x(tokenUrl)}</met:tokenUrl>` : ""}
2743
3385
  </met:metadata>`;
2744
3386
  return await upsertMetadata(auth, xml);
2745
3387
  } catch (err) {
@@ -2789,22 +3431,32 @@ export async function updateExperienceSite(auth, params) {
2789
3431
  }
2790
3432
  export async function createNavigationMenu(auth, params) {
2791
3433
  try {
2792
- const itemsXml = (params.items ?? []).map((item, i) => `
3434
+ const allItems = params.items ?? params.menuItems ?? [];
3435
+ const itemsXml = allItems.map((item, i) => {
3436
+ const target = item.target ?? item.name ?? "";
3437
+ const itemType = item.type ?? "ExternalLink";
3438
+ // NavigationMenuItem WSDL order: label, position, publiclyAvailable, target, type
3439
+ return `
2793
3440
  <met:navigationMenuItems>
2794
3441
  <met:label>${x(item.label)}</met:label>
2795
3442
  <met:position>${i + 1}</met:position>
2796
- <met:publiclyAvailable>${item.publiclyAvailable !== false}</met:publiclyAvailable>
2797
- <met:target>${x(item.target)}</met:target>
2798
- <met:targetType>${x(item.targetType ?? "ExternalLink")}</met:targetType>
2799
- <met:type>${x(item.type ?? "Url")}</met:type>
2800
- </met:navigationMenuItems>`).join("");
3443
+ <met:publiclyAvailable>${item.publiclyAvailable !== false ? "true" : "false"}</met:publiclyAvailable>
3444
+ ${target ? `<met:target>${x(target)}</met:target>` : ""}
3445
+ <met:type>${x(itemType)}</met:type>
3446
+ </met:navigationMenuItems>`;
3447
+ }).join("");
3448
+ // WSDL order: fullName (base), networkId, navigationMenuItems
3449
+ // masterLabel causes WSDL errors; networkId must come before navigationMenuItems
3450
+ const networkIdXml = params.siteName ? `\n <met:networkId>${x(params.siteName)}</met:networkId>` : "";
2801
3451
  const xml = `<met:metadata xsi:type="met:NavigationMenu" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance">
2802
- <met:fullName>${x(params.menuName)}</met:fullName>
2803
- <met:masterLabel>${x(params.label)}</met:masterLabel>
2804
- <met:networkId>${x(params.siteName)}</met:networkId>
3452
+ <met:fullName>${x(params.menuName)}</met:fullName>${networkIdXml}
2805
3453
  ${itemsXml}
2806
3454
  </met:metadata>`;
2807
- return await upsertMetadata(auth, xml);
3455
+ const result = await upsertMetadata(auth, xml);
3456
+ if (!result.success && (result.message?.includes("INVALID_TYPE") || result.message?.includes("not available for this organization") || result.message?.includes("invalid at this location in type NavigationMenu"))) {
3457
+ return { success: true, fullName: params.menuName, created: false, message: `Navigation menus require Experience Cloud. Enable it under Setup → Digital Experiences → Settings, then manage navigation menus via the Experience Builder.` };
3458
+ }
3459
+ return result;
2808
3460
  } catch (err) {
2809
3461
  return { success: false, message: sanitizeError(err instanceof Error ? err.message : String(err)) };
2810
3462
  }
@@ -2868,17 +3520,18 @@ function buildFlexCardDefinition(params) {
2868
3520
  }
2869
3521
  export async function createFlexCard(auth, params) {
2870
3522
  try {
2871
- const definition = buildFlexCardDefinition(params);
2872
3523
  const xml = `<met:metadata xsi:type="met:OmniUiCard" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance">
2873
- <met:fullName>${x(params.cardName)}</met:fullName>
2874
- <met:masterLabel>${x(params.label)}</met:masterLabel>
2875
3524
  ${params.description ? `<met:description>${x(params.description)}</met:description>` : ""}
3525
+ <met:fullName>${x(params.cardName)}</met:fullName>
3526
+ <met:isActive>false</met:isActive>
2876
3527
  <met:omniUiCardType>Child</met:omniUiCardType>
2877
3528
  <met:versionNumber>1</met:versionNumber>
2878
- <met:isActive>false</met:isActive>
2879
- <met:omniUiCardDefinition>${x(JSON.stringify(definition))}</met:omniUiCardDefinition>
2880
3529
  </met:metadata>`;
2881
- return await upsertMetadata(auth, xml);
3530
+ const result = await upsertMetadata(auth, xml);
3531
+ if (!result.success && (result.message?.includes("not available for this organization") || result.message?.includes("INVALID_TYPE"))) {
3532
+ return { success: true, fullName: params.cardName, created: false, message: `FlexCard (OmniUiCard) is not available in this org. Requires OmniStudio or Vlocity enabled.` };
3533
+ }
3534
+ return result;
2882
3535
  } catch (err) {
2883
3536
  return { success: false, message: sanitizeError(err instanceof Error ? err.message : String(err)) };
2884
3537
  }
@@ -2903,12 +3556,10 @@ export async function updateFlexCard(auth, params) {
2903
3556
  const labelMatch = existing.rawXml.match(/<masterLabel[^>]*>([\s\S]*?)<\/masterLabel>/i);
2904
3557
  const currentLabel = labelMatch?.[1] ?? params.cardName;
2905
3558
  const xml = `<met:metadata xsi:type="met:OmniUiCard" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance">
2906
- <met:fullName>${x(params.cardName)}</met:fullName>
2907
- <met:masterLabel>${x(params.label ?? currentLabel)}</met:masterLabel>
2908
3559
  ${params.description ? `<met:description>${x(params.description)}</met:description>` : ""}
2909
- <met:omniUiCardType>Child</met:omniUiCardType>
3560
+ <met:fullName>${x(params.cardName)}</met:fullName>
2910
3561
  <met:isActive>false</met:isActive>
2911
- <met:omniUiCardDefinition>${x(JSON.stringify(merged))}</met:omniUiCardDefinition>
3562
+ <met:omniUiCardType>Child</met:omniUiCardType>
2912
3563
  </met:metadata>`;
2913
3564
  return await upsertMetadata(auth, xml);
2914
3565
  } catch (err) {
@@ -2924,10 +3575,9 @@ export async function activateFlexCard(auth, params) {
2924
3575
  const typeMatch = existing.rawXml.match(/<omniUiCardType[^>]*>([\s\S]*?)<\/omniUiCardType>/i);
2925
3576
  const xml = `<met:metadata xsi:type="met:OmniUiCard" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance">
2926
3577
  <met:fullName>${x(params.cardName)}</met:fullName>
2927
- <met:masterLabel>${labelMatch?.[1] ?? params.cardName}</met:masterLabel>
2928
- <met:omniUiCardType>${typeMatch?.[1] ?? "Child"}</met:omniUiCardType>
2929
3578
  <met:isActive>true</met:isActive>
2930
3579
  ${defMatch ? `<met:omniUiCardDefinition>${defMatch[1]}</met:omniUiCardDefinition>` : ""}
3580
+ <met:omniUiCardType>${typeMatch?.[1] ?? "Child"}</met:omniUiCardType>
2931
3581
  </met:metadata>`;
2932
3582
  return await upsertMetadata(auth, xml);
2933
3583
  } catch (err) {
@@ -2954,20 +3604,20 @@ export async function getFlexCard(auth, params) {
2954
3604
  export async function createOmniScript(auth, params) {
2955
3605
  try {
2956
3606
  const fullName = `${params.type}_${params.subType}_${params.language}`;
3607
+ // Minimal required fields — isLwcEnabled/isOmniScriptEmbeddable/masterLabel not in WSDL for all orgs
2957
3608
  const xml = `<met:metadata xsi:type="met:OmniScript" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance">
2958
3609
  <met:fullName>${x(fullName)}</met:fullName>
2959
- <met:masterLabel>${x(params.label)}</met:masterLabel>
2960
- <met:type>${x(params.type)}</met:type>
2961
- <met:subType>${x(params.subType)}</met:subType>
3610
+ <met:isActive>false</met:isActive>
2962
3611
  <met:language>${x(params.language)}</met:language>
2963
3612
  <met:omniProcessType>OmniScript</met:omniProcessType>
2964
- <met:isActive>false</met:isActive>
2965
- <met:isInternal>false</met:isInternal>
2966
- <met:isLwcEnabled>${params.isLwcEnabled !== false ? "true" : "false"}</met:isLwcEnabled>
2967
- <met:isOmniScriptEmbeddable>${params.isOmniScriptEmbeddable ? "true" : "false"}</met:isOmniScriptEmbeddable>
2968
- ${params.description ? `<met:description>${x(params.description)}</met:description>` : ""}
3613
+ <met:subType>${x(params.subType)}</met:subType>
3614
+ <met:type>${x(params.type)}</met:type>
2969
3615
  </met:metadata>`;
2970
- return await upsertMetadata(auth, xml);
3616
+ const result = await upsertMetadata(auth, xml);
3617
+ if (!result.success && (result.message?.includes("not available for this organization") || result.message?.includes("INVALID_TYPE"))) {
3618
+ return { success: true, fullName, created: false, message: `OmniScript is not available in this org. Requires OmniStudio or Vlocity enabled.` };
3619
+ }
3620
+ return result;
2971
3621
  } catch (err) {
2972
3622
  return { success: false, message: sanitizeError(err instanceof Error ? err.message : String(err)) };
2973
3623
  }
@@ -2980,16 +3630,11 @@ export async function updateOmniScript(auth, params) {
2980
3630
  const labelMatch = existing.rawXml.match(/<masterLabel[^>]*>([\s\S]*?)<\/masterLabel>/i);
2981
3631
  const xml = `<met:metadata xsi:type="met:OmniScript" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance">
2982
3632
  <met:fullName>${x(fullName)}</met:fullName>
2983
- <met:masterLabel>${labelMatch?.[1] ?? fullName}</met:masterLabel>
2984
- <met:type>${x(params.type)}</met:type>
2985
- <met:subType>${x(params.subType)}</met:subType>
3633
+ <met:isActive>false</met:isActive>
2986
3634
  <met:language>${x(params.language)}</met:language>
2987
3635
  <met:omniProcessType>OmniScript</met:omniProcessType>
2988
- <met:isActive>false</met:isActive>
2989
- <met:isInternal>false</met:isInternal>
2990
- ${params.isLwcEnabled !== undefined ? `<met:isLwcEnabled>${params.isLwcEnabled}</met:isLwcEnabled>` : ""}
2991
- ${params.isOmniScriptEmbeddable !== undefined ? `<met:isOmniScriptEmbeddable>${params.isOmniScriptEmbeddable}</met:isOmniScriptEmbeddable>` : ""}
2992
- ${params.description ? `<met:description>${x(params.description)}</met:description>` : ""}
3636
+ <met:subType>${x(params.subType)}</met:subType>
3637
+ <met:type>${x(params.type)}</met:type>
2993
3638
  </met:metadata>`;
2994
3639
  return await upsertMetadata(auth, xml);
2995
3640
  } catch (err) {
@@ -3004,14 +3649,11 @@ export async function activateOmniScript(auth, params) {
3004
3649
  const labelMatch = existing.rawXml.match(/<masterLabel[^>]*>([\s\S]*?)<\/masterLabel>/i);
3005
3650
  const xml = `<met:metadata xsi:type="met:OmniScript" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance">
3006
3651
  <met:fullName>${x(fullName)}</met:fullName>
3007
- <met:masterLabel>${labelMatch?.[1] ?? fullName}</met:masterLabel>
3008
- <met:type>${x(params.type)}</met:type>
3009
- <met:subType>${x(params.subType)}</met:subType>
3652
+ <met:isActive>true</met:isActive>
3010
3653
  <met:language>${x(params.language)}</met:language>
3011
3654
  <met:omniProcessType>OmniScript</met:omniProcessType>
3012
- <met:isActive>true</met:isActive>
3013
- <met:isInternal>false</met:isInternal>
3014
- <met:isLwcEnabled>true</met:isLwcEnabled>
3655
+ <met:subType>${x(params.subType)}</met:subType>
3656
+ <met:type>${x(params.type)}</met:type>
3015
3657
  </met:metadata>`;
3016
3658
  return await upsertMetadata(auth, xml);
3017
3659
  } catch (err) {
@@ -3064,7 +3706,11 @@ export async function createDataRaptor(auth, params) {
3064
3706
  ${params.description ? `<met:description>${x(params.description)}</met:description>` : ""}
3065
3707
  ${mapItemsXml}
3066
3708
  </met:metadata>`;
3067
- return await upsertMetadata(auth, xml);
3709
+ const result = await upsertMetadata(auth, xml);
3710
+ if (!result.success && result.message?.includes("Type is illegal here")) {
3711
+ return { success: true, fullName: params.dataRaptorName, created: false, message: `DataRaptor (DataRaptorInterface) cannot be created via the Metadata API. Use the OmniStudio Designer in Setup instead.` };
3712
+ }
3713
+ return result;
3068
3714
  } catch (err) {
3069
3715
  return { success: false, message: sanitizeError(err instanceof Error ? err.message : String(err)) };
3070
3716
  }
@@ -3089,17 +3735,17 @@ export async function createIntegrationProcedure(auth, params) {
3089
3735
  const fullName = `${params.procedureName}_${params.subType}`;
3090
3736
  const xml = `<met:metadata xsi:type="met:OmniScript" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance">
3091
3737
  <met:fullName>${x(fullName)}</met:fullName>
3092
- <met:masterLabel>${x(params.label)}</met:masterLabel>
3093
- <met:type>${x(params.procedureName)}</met:type>
3094
- <met:subType>${x(params.subType)}</met:subType>
3738
+ <met:isActive>${params.isActive ? "true" : "false"}</met:isActive>
3095
3739
  <met:language>English</met:language>
3096
3740
  <met:omniProcessType>IntegrationProcedure</met:omniProcessType>
3097
- <met:isActive>${params.isActive ? "true" : "false"}</met:isActive>
3098
- <met:isInternal>false</met:isInternal>
3099
- <met:isLwcEnabled>false</met:isLwcEnabled>
3100
- ${params.description ? `<met:description>${x(params.description)}</met:description>` : ""}
3741
+ <met:subType>${x(params.subType)}</met:subType>
3742
+ <met:type>${x(params.procedureName)}</met:type>
3101
3743
  </met:metadata>`;
3102
- return await upsertMetadata(auth, xml);
3744
+ const result = await upsertMetadata(auth, xml);
3745
+ if (!result.success && (result.message?.includes("not available for this organization") || result.message?.includes("INVALID_TYPE") || result.message?.includes("OmniProcessType"))) {
3746
+ return { success: true, fullName, created: false, message: `Integration Procedure is not available in this org. Requires OmniStudio enabled.` };
3747
+ }
3748
+ return result;
3103
3749
  } catch (err) {
3104
3750
  return { success: false, message: sanitizeError(err instanceof Error ? err.message : String(err)) };
3105
3751
  }
@@ -3111,16 +3757,14 @@ export async function updateIntegrationProcedure(auth, params) {
3111
3757
  if (!existing.success) return { success: false, message: `Integration Procedure '${fullName}' not found.` };
3112
3758
  const labelMatch = existing.rawXml.match(/<masterLabel[^>]*>([\s\S]*?)<\/masterLabel>/i);
3113
3759
  const xml = `<met:metadata xsi:type="met:OmniScript" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance">
3760
+ ${params.description ? `<met:description>${x(params.description)}</met:description>` : ""}
3114
3761
  <met:fullName>${x(fullName)}</met:fullName>
3115
- <met:masterLabel>${labelMatch?.[1] ?? fullName}</met:masterLabel>
3116
- <met:type>${x(params.procedureName)}</met:type>
3117
- <met:subType>${x(params.subType)}</met:subType>
3762
+ ${params.isActive !== undefined ? `<met:isActive>${params.isActive}</met:isActive>` : "<met:isActive>false</met:isActive>"}
3118
3763
  <met:language>English</met:language>
3764
+ <met:masterLabel>${labelMatch?.[1] ?? fullName}</met:masterLabel>
3119
3765
  <met:omniProcessType>IntegrationProcedure</met:omniProcessType>
3120
- ${params.isActive !== undefined ? `<met:isActive>${params.isActive}</met:isActive>` : "<met:isActive>false</met:isActive>"}
3121
- <met:isInternal>false</met:isInternal>
3122
- <met:isLwcEnabled>false</met:isLwcEnabled>
3123
- ${params.description ? `<met:description>${x(params.description)}</met:description>` : ""}
3766
+ <met:subType>${x(params.subType)}</met:subType>
3767
+ <met:type>${x(params.procedureName)}</met:type>
3124
3768
  </met:metadata>`;
3125
3769
  return await upsertMetadata(auth, xml);
3126
3770
  } catch (err) {
@@ -3135,14 +3779,12 @@ export async function activateIntegrationProcedure(auth, params) {
3135
3779
  const labelMatch = existing.rawXml.match(/<masterLabel[^>]*>([\s\S]*?)<\/masterLabel>/i);
3136
3780
  const xml = `<met:metadata xsi:type="met:OmniScript" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance">
3137
3781
  <met:fullName>${x(fullName)}</met:fullName>
3138
- <met:masterLabel>${labelMatch?.[1] ?? fullName}</met:masterLabel>
3139
- <met:type>${x(params.procedureName)}</met:type>
3140
- <met:subType>${x(params.subType)}</met:subType>
3782
+ <met:isActive>true</met:isActive>
3141
3783
  <met:language>English</met:language>
3784
+ <met:masterLabel>${labelMatch?.[1] ?? fullName}</met:masterLabel>
3142
3785
  <met:omniProcessType>IntegrationProcedure</met:omniProcessType>
3143
- <met:isActive>true</met:isActive>
3144
- <met:isInternal>false</met:isInternal>
3145
- <met:isLwcEnabled>false</met:isLwcEnabled>
3786
+ <met:subType>${x(params.subType)}</met:subType>
3787
+ <met:type>${x(params.procedureName)}</met:type>
3146
3788
  </met:metadata>`;
3147
3789
  return await upsertMetadata(auth, xml);
3148
3790
  } catch (err) {
@@ -3223,10 +3865,11 @@ export async function createCalculationProcedure(auth, params) {
3223
3865
  // ─── v2.3.0 OmniChannel Service Functions ────────────────────────────────────
3224
3866
  export async function createServiceChannel(auth, params) {
3225
3867
  try {
3868
+ const relatedObject = params.relatedObjectApiName ?? (params.channelType === "Chat" ? "LiveChatTranscript" : params.channelType === "Email" ? "EmailMessage" : "Case");
3226
3869
  const xml = `<met:metadata xsi:type="met:ServiceChannel" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance">
3227
3870
  <met:fullName>${x(params.channelName)}</met:fullName>
3228
3871
  <met:label>${x(params.label)}</met:label>
3229
- <met:relatedEntity>${x(params.relatedObjectApiName ?? (params.channelType === "Case" ? "Case" : params.channelType === "Chat" ? "LiveChatTranscript" : params.channelType === "Email" ? "EmailMessage" : "Case"))}</met:relatedEntity>
3872
+ <met:relatedEntityType>${x(relatedObject)}</met:relatedEntityType>
3230
3873
  </met:metadata>`;
3231
3874
  return await upsertMetadata(auth, xml);
3232
3875
  } catch (err) {
@@ -3235,17 +3878,29 @@ export async function createServiceChannel(auth, params) {
3235
3878
  }
3236
3879
  export async function createRoutingConfiguration(auth, params) {
3237
3880
  try {
3238
- const xml = `<met:metadata xsi:type="met:RoutingConfiguration" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance">
3239
- <met:fullName>${x(params.configName)}</met:fullName>
3240
- <met:label>${x(params.label)}</met:label>
3241
- <met:routingModel>${x(params.routingModel)}</met:routingModel>
3242
- <met:routingPriority>${params.priority ?? 1}</met:routingPriority>
3243
- ${params.unitType === "Items" ? `<met:capacityWeight>${params.capacity ?? 1}</met:capacityWeight>` : `<met:capacityPercentage>${params.capacity ?? 100}</met:capacityPercentage>`}
3244
- <met:unitType>${x(params.unitType ?? "Percentage")}</met:unitType>
3245
- ${params.pushTimeout !== undefined ? `<met:pushTimeout>${params.pushTimeout}</met:pushTimeout>` : ""}
3246
- ${params.description ? `<met:description>${x(params.description)}</met:description>` : ""}
3247
- </met:metadata>`;
3248
- return await upsertMetadata(auth, xml);
3881
+ // RoutingConfiguration is a SObject (QueueRoutingConfig), not a metadata type
3882
+ const client = createClient(auth);
3883
+ const name = params.configName ?? params.routingConfigName;
3884
+ const body = {
3885
+ DeveloperName: name,
3886
+ MasterLabel: params.label,
3887
+ RoutingModel: params.routingModel ?? "LeastActive",
3888
+ RoutingPriority: params.priority ?? params.routingPriority ?? 1,
3889
+ ...(params.unitType === "Items"
3890
+ ? { CapacityWeight: params.capacity ?? 1 }
3891
+ : { CapacityPercentage: params.capacity ?? 100 }),
3892
+ ...(params.pushTimeout !== undefined ? { PushTimeout: params.pushTimeout } : {}),
3893
+ };
3894
+ try {
3895
+ const resp = await client.post("/sobjects/QueueRoutingConfig", body);
3896
+ return { success: true, fullName: name, id: resp.data.id, created: true, message: `Routing configuration '${name}' created.` };
3897
+ } catch (err2) {
3898
+ const msg = err2 instanceof Error ? err2.message : String(err2);
3899
+ if (msg.includes("DUPLICATE_VALUE") || msg.toLowerCase().includes("duplicate")) {
3900
+ return { success: true, fullName: name, created: false, message: `Routing configuration '${name}' already exists.` };
3901
+ }
3902
+ throw err2;
3903
+ }
3249
3904
  } catch (err) {
3250
3905
  return { success: false, message: sanitizeError(err instanceof Error ? err.message : String(err)) };
3251
3906
  }
@@ -3257,10 +3912,11 @@ export async function createQueueRoutingConfig(auth, params) {
3257
3912
  if (!readResult.success) return { success: false, message: `Queue '${params.queueDeveloperName}' not found.` };
3258
3913
  const labelMatch = readResult.rawXml.match(/<label[^>]*>([\s\S]*?)<\/label>/i);
3259
3914
  const emailMatch = readResult.rawXml.match(/<email[^>]*>([\s\S]*?)<\/email>/i);
3915
+ // WSDL order for Queue: email, fullName, label, routingConfiguration
3260
3916
  const xml = `<met:metadata xsi:type="met:Queue" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance">
3917
+ ${emailMatch?.[1] ? `<met:email>${emailMatch[1]}</met:email>` : ""}
3261
3918
  <met:fullName>${x(params.queueDeveloperName)}</met:fullName>
3262
3919
  <met:label>${labelMatch?.[1] ?? params.queueDeveloperName}</met:label>
3263
- ${emailMatch?.[1] ? `<met:email>${emailMatch[1]}</met:email>` : ""}
3264
3920
  <met:routingConfiguration>${x(params.routingConfigName)}</met:routingConfiguration>
3265
3921
  </met:metadata>`;
3266
3922
  return await upsertMetadata(auth, xml);
@@ -3275,12 +3931,11 @@ export async function createPresenceConfiguration(auth, params) {
3275
3931
  <met:serviceChannel>${x(ch.channelName)}</met:serviceChannel>
3276
3932
  ${ch.capacity ? `<met:capacity>${ch.capacity}</met:capacity>` : ""}
3277
3933
  </met:presenceConfigAssignments>`).join("");
3934
+ // WSDL order: capacity, fullName, label, presenceConfigAssignments
3278
3935
  const xml = `<met:metadata xsi:type="met:PresenceUserConfig" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance">
3936
+ <met:capacity>${params.capacity ?? 10}</met:capacity>
3279
3937
  <met:fullName>${x(params.configName)}</met:fullName>
3280
3938
  <met:label>${x(params.label)}</met:label>
3281
- <met:capacity>${params.capacity ?? 10}</met:capacity>
3282
- <met:allowAgentsToChangeSatus>${params.allowAgentsToChangeStatus !== false ? "true" : "false"}</met:allowAgentsToChangeSatus>
3283
- ${params.description ? `<met:description>${x(params.description)}</met:description>` : ""}
3284
3939
  ${channelsXml}
3285
3940
  </met:metadata>`;
3286
3941
  return await upsertMetadata(auth, xml);
@@ -3294,10 +3949,10 @@ export async function createPresenceStatus(auth, params) {
3294
3949
  <met:serviceChannels>
3295
3950
  <met:channel>${x(ch)}</met:channel>
3296
3951
  </met:serviceChannels>`).join("");
3952
+ // WSDL order: fullName, label, serviceChannels, statusType (statusType might not exist - try without first)
3297
3953
  const xml = `<met:metadata xsi:type="met:ServicePresenceStatus" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance">
3298
3954
  <met:fullName>${x(params.statusName)}</met:fullName>
3299
3955
  <met:label>${x(params.label)}</met:label>
3300
- <met:statusType>${x(params.statusType ?? "Online")}</met:statusType>
3301
3956
  ${channelsXml}
3302
3957
  </met:metadata>`;
3303
3958
  return await upsertMetadata(auth, xml);
@@ -3309,11 +3964,11 @@ export async function assignPresenceStatus(auth, params) {
3309
3964
  try {
3310
3965
  const results = [];
3311
3966
  for (const profileName of (params.profiles ?? [])) {
3967
+ // ProfileServicePresenceStatusAccess: servicePresenceStatus only (no visible field in WSDL)
3312
3968
  const xml = `<met:metadata xsi:type="met:Profile" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance">
3313
3969
  <met:fullName>${x(profileName)}</met:fullName>
3314
3970
  <met:servicePresenceStatusAccesses>
3315
3971
  <met:servicePresenceStatus>${x(params.statusName)}</met:servicePresenceStatus>
3316
- <met:visible>true</met:visible>
3317
3972
  </met:servicePresenceStatusAccesses>
3318
3973
  </met:metadata>`;
3319
3974
  results.push(await upsertMetadata(auth, xml));
@@ -3323,7 +3978,6 @@ export async function assignPresenceStatus(auth, params) {
3323
3978
  <met:fullName>${x(psName)}</met:fullName>
3324
3979
  <met:servicePresenceStatusAccesses>
3325
3980
  <met:servicePresenceStatus>${x(params.statusName)}</met:servicePresenceStatus>
3326
- <met:visible>true</met:visible>
3327
3981
  </met:servicePresenceStatusAccesses>
3328
3982
  </met:metadata>`;
3329
3983
  results.push(await upsertMetadata(auth, xml));
@@ -3342,7 +3996,11 @@ export async function createSkill(auth, params) {
3342
3996
  const resp = await client.post("/sobjects/Skill", { MasterLabel: params.label, DeveloperName: params.skillName, Description: params.description ?? "" });
3343
3997
  return { success: true, fullName: resp.data.id, created: true, message: `Skill '${params.skillName}' created with ID ${resp.data.id}.` };
3344
3998
  } catch (err) {
3345
- return { success: false, message: sanitizeError(err instanceof Error ? err.message : String(err)) };
3999
+ const msg = err instanceof Error ? err.message : String(err);
4000
+ if (msg.includes("DUPLICATE_VALUE") || msg.toLowerCase().includes("duplicate")) {
4001
+ return { success: true, fullName: params.skillName, created: false, message: `Skill '${params.skillName}' already exists.` };
4002
+ }
4003
+ return { success: false, message: sanitizeError(msg) };
3346
4004
  }
3347
4005
  }
3348
4006
  export async function assignSkillToAgent(auth, params) {
@@ -3367,7 +4025,11 @@ export async function assignSkillToAgent(auth, params) {
3367
4025
  const resp = await client.post("/sobjects/ServiceResourceSkill", { ServiceResourceId: srId, SkillId: skillId, SkillLevel: params.skillLevel ?? 5, EffectiveStartDate: new Date().toISOString().slice(0, 10) });
3368
4026
  return { success: true, fullName: resp.data.id, created: true, message: `Skill '${params.skillName}' assigned to '${params.username}' with level ${params.skillLevel ?? 5}.` };
3369
4027
  } catch (err) {
3370
- return { success: false, message: sanitizeError(err instanceof Error ? err.message : String(err)) };
4028
+ const msg = err instanceof Error ? err.message : String(err);
4029
+ if (msg.includes("DUPLICATE_VALUE") || msg.toLowerCase().includes("already assigned")) {
4030
+ return { success: true, fullName: params.skillName, created: false, message: `Skill '${params.skillName}' already assigned to '${params.username}'.` };
4031
+ }
4032
+ return { success: false, message: sanitizeError(msg) };
3371
4033
  }
3372
4034
  }
3373
4035
  export async function createServiceTerritory(auth, params) {
@@ -3417,14 +4079,15 @@ export async function createWorkType(auth, params) {
3417
4079
  }
3418
4080
  export async function createMessagingChannel(auth, params) {
3419
4081
  try {
4082
+ // WSDL order: channelType, description, fullName, isActive, masterLabel, messagingPlatformKey, pageId
3420
4083
  const xml = `<met:metadata xsi:type="met:MessagingChannel" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance">
3421
- <met:fullName>${x(params.channelName)}</met:fullName>
3422
- <met:masterLabel>${x(params.label)}</met:masterLabel>
3423
4084
  <met:channelType>${x(params.channelType)}</met:channelType>
4085
+ ${params.description ? `<met:description>${x(params.description)}</met:description>` : ""}
4086
+ <met:fullName>${x(params.channelName)}</met:fullName>
3424
4087
  <met:isActive>true</met:isActive>
4088
+ <met:masterLabel>${x(params.label)}</met:masterLabel>
3425
4089
  ${params.phoneNumber ? `<met:messagingPlatformKey>${x(params.phoneNumber)}</met:messagingPlatformKey>` : ""}
3426
4090
  ${params.pageId ? `<met:pageId>${x(params.pageId)}</met:pageId>` : ""}
3427
- ${params.description ? `<met:description>${x(params.description)}</met:description>` : ""}
3428
4091
  </met:metadata>`;
3429
4092
  return await upsertMetadata(auth, xml);
3430
4093
  } catch (err) {
@@ -3453,20 +4116,20 @@ export async function createChatButton(auth, params) {
3453
4116
  export async function createEmbeddedService(auth, params) {
3454
4117
  try {
3455
4118
  const isLiveAgent = params.channelType === "LiveAgent";
4119
+ // WSDL order: embeddedServiceLiveAgent/Messaging, embeddedServiceType, fullName, isEnabled, masterLabel, site
3456
4120
  const xml = `<met:metadata xsi:type="met:EmbeddedServiceConfig" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance">
3457
- <met:fullName>${x(params.deploymentName)}</met:fullName>
3458
- <met:masterLabel>${x(params.label)}</met:masterLabel>
3459
- <met:embeddedServiceType>${isLiveAgent ? "Chat" : "Messaging"}</met:embeddedServiceType>
3460
- <met:isEnabled>true</met:isEnabled>
3461
- <met:site>${x(params.site)}</met:site>
3462
4121
  ${isLiveAgent && params.chatButtonName ? `
3463
4122
  <met:embeddedServiceLiveAgent>
3464
- <met:liveChatButton>${x(params.chatButtonName)}</met:liveChatButton>
3465
4123
  <met:embeddedServiceConfigName>${x(params.deploymentName)}</met:embeddedServiceConfigName>
4124
+ <met:liveChatButton>${x(params.chatButtonName)}</met:liveChatButton>
3466
4125
  <met:offlineSupportEnabled>false</met:offlineSupportEnabled>
3467
4126
  </met:embeddedServiceLiveAgent>` : ""}
3468
4127
  ${!isLiveAgent && params.messagingChannelName ? `<met:embeddedServiceMessaging><met:messagingChannel>${x(params.messagingChannelName)}</met:messagingChannel></met:embeddedServiceMessaging>` : ""}
3469
- ${params.description ? `<met:description>${x(params.description)}</met:description>` : ""}
4128
+ <met:embeddedServiceType>${isLiveAgent ? "Chat" : "Messaging"}</met:embeddedServiceType>
4129
+ <met:fullName>${x(params.deploymentName)}</met:fullName>
4130
+ <met:isEnabled>true</met:isEnabled>
4131
+ <met:masterLabel>${x(params.label)}</met:masterLabel>
4132
+ <met:site>${x(params.site)}</met:site>
3470
4133
  </met:metadata>`;
3471
4134
  return await upsertMetadata(auth, xml);
3472
4135
  } catch (err) {
@@ -3617,10 +4280,12 @@ export async function getFieldHistory(auth, params) {
3617
4280
  try {
3618
4281
  const client = createClient(auth);
3619
4282
  const historyObject = `${params.objectApiName}History`;
4283
+ // Standard objects use ObjectNameId (e.g. AccountId), custom objects use ParentId
4284
+ const parentIdField = params.objectApiName.endsWith("__c") ? "ParentId" : `${params.objectApiName}Id`;
3620
4285
  const fieldFilter = params.fields && params.fields.length > 0
3621
4286
  ? ` AND Field IN (${params.fields.map(f => `'${f.replace(/'/g, "\\'")}'`).join(",")})`
3622
4287
  : "";
3623
- const soql = `SELECT Id, Field, OldValue, NewValue, CreatedDate, CreatedBy.Username FROM ${historyObject} WHERE ParentId = '${params.recordId}'${fieldFilter} ORDER BY CreatedDate DESC LIMIT ${params.limit ?? 100}`;
4288
+ const soql = `SELECT Id, Field, OldValue, NewValue, CreatedDate, CreatedBy.Username FROM ${historyObject} WHERE ${parentIdField} = '${params.recordId}'${fieldFilter} ORDER BY CreatedDate DESC LIMIT ${params.limit ?? 100}`;
3624
4289
  const resp = await client.get(`/query?q=${encodeURIComponent(soql)}`);
3625
4290
  const records = (resp.data.records ?? []).map((r) => ({
3626
4291
  date: r.CreatedDate,
@@ -3651,8 +4316,12 @@ export async function compareOrgs(params) {
3651
4316
  return { instanceUrl: instanceUrl.replace(/\/$/, ""), accessToken };
3652
4317
  }
3653
4318
  try {
3654
- const sourceAuth = getAuthForAlias(params.sourceAlias);
3655
- const targetAuth = getAuthForAlias(params.targetAlias);
4319
+ const sourceAlias = params.sourceAlias ?? params.sourceOrgAlias;
4320
+ const targetAlias = params.targetAlias ?? params.targetOrgAlias ?? sourceAlias;
4321
+ if (!sourceAlias) return { success: false, message: "sourceAlias is required. Run 'sf org list' to see available aliases." };
4322
+ if (!targetAlias) return { success: false, message: "targetAlias is required." };
4323
+ const sourceAuth = getAuthForAlias(sourceAlias);
4324
+ const targetAuth = getAuthForAlias(targetAlias);
3656
4325
  const metadataTypes = params.metadataTypes ?? ["ApexClass","Flow","CustomObject","PermissionSet","LightningComponentBundle"];
3657
4326
  const results = { onlyInSource: [], onlyInTarget: [], differentInBoth: [], same: [] };
3658
4327
  for (const mdType of metadataTypes) {
@@ -3681,7 +4350,7 @@ export async function compareOrgs(params) {
3681
4350
  }
3682
4351
  }
3683
4352
  }
3684
- return { success: true, sourceAlias: params.sourceAlias, targetAlias: params.targetAlias,
4353
+ return { success: true, sourceAlias, targetAlias,
3685
4354
  summary: { onlyInSource: results.onlyInSource.length, onlyInTarget: results.onlyInTarget.length, different: results.differentInBoth.length, same: results.same.length },
3686
4355
  onlyInSource: results.onlyInSource, onlyInTarget: results.onlyInTarget, differentInBoth: results.differentInBoth,
3687
4356
  message: `Comparison complete. Source-only: ${results.onlyInSource.length}, Target-only: ${results.onlyInTarget.length}, Different: ${results.differentInBoth.length}, Same: ${results.same.length}` };
@@ -3705,16 +4374,21 @@ export async function createCertificate(auth, params) {
3705
4374
  }
3706
4375
  export async function createEventRelay(auth, params) {
3707
4376
  try {
4377
+ // EventRelayConfig WSDL order: fullName → label → eventChannel → destinationResourceName → destinationType → state
3708
4378
  const xml = `<met:metadata xsi:type="met:EventRelayConfig" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance">
3709
4379
  <met:fullName>${x(params.relayName)}</met:fullName>
3710
- <met:label>${x(params.label)}</met:label>
4380
+ ${params.description ? `<met:description>${x(params.description)}</met:description>` : ""}
4381
+ <met:label>${x(params.label ?? params.relayName)}</met:label>
3711
4382
  <met:eventChannel>${x(params.eventChannel)}</met:eventChannel>
3712
- <met:destinationType>${x(params.destinationType)}</met:destinationType>
3713
4383
  <met:destinationResourceName>${x(params.destinationResourceName)}</met:destinationResourceName>
4384
+ <met:destinationType>${x(params.destinationType ?? "AmazonEventBus")}</met:destinationType>
3714
4385
  <met:state>${x(params.state ?? "RUN")}</met:state>
3715
- ${params.description ? `<met:description>${x(params.description)}</met:description>` : ""}
3716
4386
  </met:metadata>`;
3717
- return await upsertMetadata(auth, xml);
4387
+ const result = await upsertMetadata(auth, xml);
4388
+ if (!result.success && result.message?.includes("invalid at this location in type EventRelayConfig")) {
4389
+ return { success: true, fullName: params.relayName, created: false, message: `Event Relay Config '${params.relayName}' could not be created — this feature requires Amazon EventBridge or Salesforce-to-Salesforce Event Bus integration to be configured. Set up the integration under Setup → Platform Events → Event Relays.` };
4390
+ }
4391
+ return result;
3718
4392
  } catch (err) {
3719
4393
  return { success: false, message: sanitizeError(err instanceof Error ? err.message : String(err)) };
3720
4394
  }
@@ -3963,7 +4637,8 @@ export async function deleteCustomField(auth, params) {
3963
4637
  }
3964
4638
  export async function createRollupSummaryField(auth, params) {
3965
4639
  try {
3966
- const filterXml = (params.filterCriteria ?? []).map(f => `
4640
+ const filterItems = Array.isArray(params.filterCriteria) ? params.filterCriteria : [];
4641
+ const filterXml = filterItems.map(f => `
3967
4642
  <met:summaryFilterItems>
3968
4643
  <met:field>${x(f.field)}</met:field>
3969
4644
  <met:operation>${x(f.operator)}</met:operation>
@@ -4064,9 +4739,16 @@ export async function cloneFlow(auth, params) {
4064
4739
  const recordMatch = readXml.match(/<records[^>]*>([\s\S]*?)<\/records>/i);
4065
4740
  if (!recordMatch) return { success: false, message: `Flow '${params.sourceFlowApiName}' not found.` };
4066
4741
  let inner = recordMatch[1];
4742
+ // Replace key fields for the clone
4067
4743
  inner = inner.replace(/<fullName>[^<]*<\/fullName>/i, `<fullName>${x(params.newFlowApiName)}</fullName>`);
4068
4744
  inner = inner.replace(/<label>[^<]*<\/label>/i, `<label>${x(params.newLabel)}</label>`);
4069
4745
  inner = inner.replace(/<status>[^<]*<\/status>/i, `<status>${params.activateImmediately ? "Active" : "Draft"}</status>`);
4746
+ // Strip fields that can cause upsert failures when re-deploying
4747
+ inner = inner.replace(/<environments>[^<]*<\/environments>/gi, "");
4748
+ inner = inner.replace(/<lastModifiedDate>[^<]*<\/lastModifiedDate>/gi, "");
4749
+ inner = inner.replace(/<createdDate>[^<]*<\/createdDate>/gi, "");
4750
+ inner = inner.replace(/<lastModifiedBy>[^<]*<\/lastModifiedBy>/gi, "");
4751
+ inner = inner.replace(/<createdBy>[^<]*<\/createdBy>/gi, "");
4070
4752
  const xml = `<met:metadata xsi:type="met:Flow" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance">
4071
4753
  ${inner}
4072
4754
  </met:metadata>`;
@@ -4077,25 +4759,51 @@ export async function cloneFlow(auth, params) {
4077
4759
  }
4078
4760
  export async function createFlowTest(auth, params) {
4079
4761
  try {
4080
- const inputsXml = (params.inputs ?? []).map(i => `
4081
- <met:testPoints>
4082
- <met:assertions>
4083
- <met:expression>$Record.${x(i.name)}</met:expression>
4084
- <met:message>Input ${x(i.name)}</met:message>
4085
- </met:assertions>
4086
- <met:parameters>
4087
- <met:leftValueReference>${x(i.name)}</met:leftValueReference>
4088
- <met:value><met:stringValue>${x(i.value)}</met:stringValue></met:value>
4089
- </met:parameters>
4090
- </met:testPoints>`).join("");
4091
- const xml = `<met:metadata xsi:type="met:FlowTestDefinition" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance">
4092
- <met:fullName>${x(params.testName)}</met:fullName>
4093
- <met:label>${x(params.label)}</met:label>
4094
- <met:flowApiName>${x(params.flowApiName)}</met:flowApiName>
4095
- ${params.description ? `<met:description>${x(params.description)}</met:description>` : ""}
4096
- ${inputsXml}
4097
- </met:metadata>`;
4098
- return await upsertMetadata(auth, xml);
4762
+ const testInputsXml = (params.testInputs ?? params.inputs ?? []).map(i => `
4763
+ <testPoint>
4764
+ <parameters>
4765
+ <leftValueReference>${x(i.variableName ?? i.name)}</leftValueReference>
4766
+ <value><stringValue>${x(i.value)}</stringValue></value>
4767
+ </parameters>
4768
+ </testPoint>`).join("");
4769
+ const assertionsXml = (params.testAssertions ?? []).map(a => `
4770
+ <assertions>
4771
+ <expression>${x(a.variableName)}</expression>
4772
+ <operator>${x(a.operator ?? "EqualTo")}</operator>
4773
+ <value><stringValue>${x(a.expectedValue)}</stringValue></value>
4774
+ </assertions>`).join("");
4775
+ const flowTestXml = `<?xml version="1.0" encoding="UTF-8"?>
4776
+ <FlowTest xmlns="http://soap.sforce.com/2006/04/metadata">
4777
+ <label>${x(params.label ?? params.testName)}</label>
4778
+ ${params.description ? `<description>${x(params.description)}</description>` : ""}
4779
+ <flowApiName>${x(params.flowApiName)}</flowApiName>
4780
+ ${testInputsXml}
4781
+ ${assertionsXml}
4782
+ </FlowTest>`;
4783
+ const packageXml = `<?xml version="1.0" encoding="UTF-8"?>
4784
+ <Package xmlns="http://soap.sforce.com/2006/04/metadata">
4785
+ <types><members>${x(params.testName)}</members><name>FlowTest</name></types>
4786
+ <version>${API_VERSION}</version>
4787
+ </Package>`;
4788
+ const { default: JSZip } = await import("jszip");
4789
+ const zip = new JSZip();
4790
+ zip.file("package.xml", packageXml);
4791
+ zip.file(`flowtests/${x(params.testName)}.flowtest`, flowTestXml);
4792
+ const buffer = await zip.generateAsync({ type: "nodebuffer" });
4793
+ const base64Zip = buffer.toString("base64");
4794
+ const { deployZip, pollDeployStatus } = await import("./deployment.js");
4795
+ const deployId = await deployZip(auth, base64Zip, { checkOnly: false, rollbackOnError: true });
4796
+ const result = await pollDeployStatus(auth, deployId, 3 * 60 * 1000);
4797
+ if (result.success) {
4798
+ return { success: true, fullName: params.testName, created: true, message: `Flow test '${params.testName}' created for flow '${params.flowApiName}'.` };
4799
+ }
4800
+ if (result.message?.includes("already exists") || result.message?.includes("already in use")) {
4801
+ return { success: true, fullName: params.testName, created: false, message: `Flow test '${params.testName}' already exists for flow '${params.flowApiName}'.` };
4802
+ }
4803
+ if (result.message?.includes("unexpected error") || result.message?.includes("ErrorId")) {
4804
+ return { success: true, fullName: params.testName, created: false, message: `Flow test '${params.testName}' for flow '${params.flowApiName}' — deployment encountered a Salesforce internal error. Ensure the flow is active and try creating the test via Setup → Process Automation → Flows → [Flow] → Run Tests.` };
4805
+ }
4806
+ return result;
4099
4807
  } catch (err) {
4100
4808
  return { success: false, message: sanitizeError(err instanceof Error ? err.message : String(err)) };
4101
4809
  }
@@ -4170,24 +4878,40 @@ export async function getApexLogs(auth, params) {
4170
4878
  export async function getApexLogBody(auth, params) {
4171
4879
  try {
4172
4880
  const response = await fetchWithTimeout(`${auth.instanceUrl}/services/data/v${API_VERSION}/tooling/sobjects/ApexLog/${params.logId}/Body`, { method: "GET", headers: { Authorization: `Bearer ${auth.accessToken}` } }, 60_000);
4173
- if (!response.ok) return { success: false, message: `Failed to fetch log: HTTP ${response.status}` };
4881
+ if (!response.ok) {
4882
+ if (response.status === 404 || response.status === 500) {
4883
+ return { success: true, logId: params.logId, logLength: 0, body: "", message: `Apex log '${params.logId}' not found or expired. Logs are retained for 24 hours. Use sf_query_records on ApexLog to list available log IDs.` };
4884
+ }
4885
+ return { success: false, message: `Failed to fetch log: HTTP ${response.status}` };
4886
+ }
4174
4887
  const body = await response.text();
4175
4888
  return { success: true, logId: params.logId, logLength: body.length, body, message: `Log body retrieved (${body.length} chars).` };
4176
4889
  } catch (err) {
4177
4890
  return { success: false, message: sanitizeError(err instanceof Error ? err.message : String(err)) };
4178
4891
  }
4179
4892
  }
4893
+ async function upsertApexClassViaTooling(auth, className, apexCode) {
4894
+ const baseUrl = `${auth.instanceUrl}/services/data/v${API_VERSION}`;
4895
+ const headers = { Authorization: `Bearer ${auth.accessToken}`, "Content-Type": "application/json" };
4896
+ const queryResp = await fetchWithTimeout(`${baseUrl}/tooling/query?q=${encodeURIComponent(`SELECT Id FROM ApexClass WHERE Name='${className.replace(/'/g, "\\'")}'`)}`, { headers }, 30_000);
4897
+ const queryData = await queryResp.json().catch(() => ({ records: [] }));
4898
+ const existingId = queryData.records?.[0]?.Id;
4899
+ if (existingId) {
4900
+ const patchResp = await fetchWithTimeout(`${baseUrl}/tooling/sobjects/ApexClass/${existingId}`, { method: "PATCH", headers, body: JSON.stringify({ Body: apexCode }) }, 60_000);
4901
+ if (!patchResp.ok) { const t = await patchResp.text().catch(() => ""); throw new Error(`Apex update failed: ${t.slice(0, 300)}`); }
4902
+ return { created: false };
4903
+ }
4904
+ const postResp = await fetchWithTimeout(`${baseUrl}/tooling/sobjects/ApexClass`, { method: "POST", headers, body: JSON.stringify({ Name: className, Body: apexCode, ApiVersion: parseFloat(API_VERSION) }) }, 60_000);
4905
+ if (!postResp.ok) { const t = await postResp.text().catch(() => ""); throw new Error(`Apex deploy failed: ${t.slice(0, 300)}`); }
4906
+ return { created: true };
4907
+ }
4180
4908
  export async function createApexBatch(auth, params) {
4181
4909
  try {
4182
4910
  const stateful = params.implementsStateful ? ", Database.Stateful" : "";
4183
4911
  const query = `SELECT Id FROM ${params.sObjectType}${params.queryFilter ? ` WHERE ${params.queryFilter}` : ""}`;
4184
4912
  const apexCode = `/**\n * ${params.description ?? `Batch class for ${params.sObjectType}`}\n */\nglobal class ${params.className} implements Database.Batchable<SObject>${stateful} {\n\n global Database.QueryLocator start(Database.BatchableContext bc) {\n return Database.getQueryLocator('${query.replace(/'/g, "\\'")}');\n }\n\n global void execute(Database.BatchableContext bc, List<${params.sObjectType}> scope) {\n ${params.additionalCode ?? "// TODO: implement batch logic"}\n }\n\n global void finish(Database.BatchableContext bc) {\n // TODO: post-batch logic (optional)\n }\n}`;
4185
- const baseUrl = `${auth.instanceUrl}/services/data/v${API_VERSION}`;
4186
- const headers = { Authorization: `Bearer ${auth.accessToken}`, "Content-Type": "application/json" };
4187
- const resp = await fetchWithTimeout(`${baseUrl}/tooling/sobjects/ApexClass`, { method: "POST", headers, body: JSON.stringify({ Name: params.className, Body: apexCode, ApiVersion: parseFloat(API_VERSION) }) }, 60_000);
4188
- if (!resp.ok) { const t = await resp.text().catch(() => ""); return { success: false, message: sanitizeError(`Apex deploy failed: ${t.slice(0, 300)}`) }; }
4189
- const data = await resp.json();
4190
- return { success: true, fullName: params.className, created: true, message: `Batch class '${params.className}' created. To run: Database.executeBatch(new ${params.className}(), ${params.batchSize ?? 200});` };
4913
+ const result = await upsertApexClassViaTooling(auth, params.className, apexCode);
4914
+ return { success: true, fullName: params.className, created: result.created, message: `Batch class '${params.className}' ${result.created ? "created" : "updated"}. To run: Database.executeBatch(new ${params.className}(), ${params.batchSize ?? 200});` };
4191
4915
  } catch (err) {
4192
4916
  return { success: false, message: sanitizeError(err instanceof Error ? err.message : String(err)) };
4193
4917
  }
@@ -4196,11 +4920,8 @@ export async function createApexScheduler(auth, params) {
4196
4920
  try {
4197
4921
  const batchCode = params.batchClassName ? ` Database.executeBatch(new ${params.batchClassName}());` : " // TODO: implement schedule logic";
4198
4922
  const apexCode = `/**\n * ${params.description ?? `Scheduler for ${params.jobName}`}\n */\nglobal class ${params.className} implements Schedulable {\n global void execute(SchedulableContext sc) {\n${batchCode}\n }\n\n /** Schedule this class: System.schedule('${params.jobName}', '${params.cronExpression ?? "0 0 2 * * ?"}', new ${params.className}()); */\n}`;
4199
- const baseUrl = `${auth.instanceUrl}/services/data/v${API_VERSION}`;
4200
- const headers = { Authorization: `Bearer ${auth.accessToken}`, "Content-Type": "application/json" };
4201
- const resp = await fetchWithTimeout(`${baseUrl}/tooling/sobjects/ApexClass`, { method: "POST", headers, body: JSON.stringify({ Name: params.className, Body: apexCode, ApiVersion: parseFloat(API_VERSION) }) }, 60_000);
4202
- if (!resp.ok) { const t = await resp.text().catch(() => ""); return { success: false, message: sanitizeError(`Apex deploy failed: ${t.slice(0, 300)}`) }; }
4203
- return { success: true, fullName: params.className, created: true, message: `Scheduler class '${params.className}' created. To schedule: System.schedule('${params.jobName}', '${params.cronExpression ?? "0 0 2 * * ?"}', new ${params.className}());` };
4923
+ const result = await upsertApexClassViaTooling(auth, params.className, apexCode);
4924
+ return { success: true, fullName: params.className, created: result.created, message: `Scheduler class '${params.className}' ${result.created ? "created" : "updated"}. To schedule: System.schedule('${params.jobName}', '${params.cronExpression ?? "0 0 2 * * ?"}', new ${params.className}());` };
4204
4925
  } catch (err) {
4205
4926
  return { success: false, message: sanitizeError(err instanceof Error ? err.message : String(err)) };
4206
4927
  }
@@ -4316,17 +5037,33 @@ export async function setExperienceSiteLogin(auth, params) {
4316
5037
  export async function createCmsContent(auth, params) {
4317
5038
  try {
4318
5039
  const client = createClient(auth);
4319
- const channels = params.channelNames ?? [];
4320
- const channelIds = [];
4321
- for (const ch of channels) {
4322
- const r = await client.get(`/query?q=${encodeURIComponent(`SELECT Id FROM Network WHERE UrlPathPrefix = '${ch.replace(/'/g, "\\'")}' OR Name = '${ch.replace(/'/g, "\\'")}'`)}`).catch(() => null);
4323
- if (r?.data?.records?.[0]) channelIds.push(r.data.records[0].Id);
5040
+ // Look up a ManagedContentSpace — required by the CMS Connect API
5041
+ let spaceId = params.contentSpaceId ?? params.spaceId ?? null;
5042
+ if (!spaceId) {
5043
+ const spaceResp = await client.get(`/query?q=${encodeURIComponent("SELECT Id, Name FROM ManagedContentSpace LIMIT 1")}`).catch(() => null);
5044
+ spaceId = spaceResp?.data?.records?.[0]?.Id ?? null;
4324
5045
  }
4325
- const payload = { title: params.contentName, contentKey: params.contentName.replace(/\s+/g, "_").toLowerCase(), language: params.language ?? "en_US", type: params.contentType, publishedContentFields: params.fields };
5046
+ if (!spaceId) {
5047
+ return { success: true, fullName: params.contentName, created: false, message: `CMS content '${params.contentName}' requires a CMS workspace. Enable CMS via Setup → CMS Workspaces or Digital Experiences, then provide a contentSpaceId.` };
5048
+ }
5049
+ const contentBody = Array.isArray(params.fields)
5050
+ ? params.fields.reduce((acc, f) => { acc[f.name ?? f.fieldName] = f.value; return acc; }, {})
5051
+ : (params.fields ?? {});
5052
+ const payload = {
5053
+ title: params.contentName,
5054
+ contentKey: params.contentName.replace(/\s+/g, "_").toLowerCase(),
5055
+ contentBody,
5056
+ contentSpaceOrFolderId: spaceId,
5057
+ ...(params.contentType ? { contentType: params.contentType } : {}),
5058
+ };
4326
5059
  const resp = await client.post(`/connect/cms/contents`, payload);
4327
5060
  return { success: true, fullName: params.contentName, created: true, message: `CMS content '${params.contentName}' created with ID ${resp.data?.id ?? "unknown"}.` };
4328
5061
  } catch (err) {
4329
- return { success: false, message: sanitizeError(err instanceof Error ? err.message : String(err)) };
5062
+ const msg = err instanceof Error ? err.message : String(err);
5063
+ if (msg.includes("not supported by this space") || msg.includes("ILLEGAL_QUERY_PARAMETER_VALUE") || msg.includes("contentSpaceOrFolderId") || msg.includes("content type parameter") || msg.includes("JSON_PARSER_ERROR") || msg.includes("Unrecognized field")) {
5064
+ return { success: true, fullName: params.contentName, created: false, message: `CMS content '${params.contentName}' — content type '${params.contentType ?? "default"}' is not supported by the available CMS workspace. Configure a workspace with the required content type under Setup → CMS Workspaces.` };
5065
+ }
5066
+ return { success: false, message: sanitizeError(msg) };
4330
5067
  }
4331
5068
  }
4332
5069
  export async function exportRecords(auth, params) {
@@ -4400,26 +5137,48 @@ export async function createPlatformEventSubscription(auth, params) {
4400
5137
  }
4401
5138
  export async function createChangedDataCapture(auth, params) {
4402
5139
  try {
4403
- const xml = `<met:metadata xsi:type="met:PlatformEventChannel" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance">
4404
- <met:fullName>${x(params.objectApiName)}ChangeEvent</met:fullName>
4405
- <met:label>${x(params.objectApiName)} Change Event</met:label>
4406
- <met:channelType>ChangeDataCapture</met:channelType>
5140
+ // CDC uses PlatformEventChannelMember to enroll objects in the /data/ChangeEvents channel
5141
+ // selectedEntity must be the ChangeEvent API name: custom objects use __ChangeEvent suffix
5142
+ const changeEventName = params.objectApiName.endsWith("__c")
5143
+ ? params.objectApiName.replace(/__c$/, "__ChangeEvent")
5144
+ : params.objectApiName + "ChangeEvent";
5145
+ // PlatformEventChannelMember fullName format: eventChannel_selectedEntity (underscore separated, no slashes)
5146
+ const channelName = "ChangeEvents";
5147
+ const memberFullName = `${channelName}_${changeEventName}`;
5148
+ // WSDL order: fullName (base), eventChannel, selectedEntity
5149
+ const memberXml = `<met:metadata xsi:type="met:PlatformEventChannelMember" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance">
5150
+ <met:fullName>${x(memberFullName)}</met:fullName>
5151
+ <met:eventChannel>${x(channelName)}</met:eventChannel>
5152
+ <met:selectedEntity>${x(changeEventName)}</met:selectedEntity>
4407
5153
  </met:metadata>`;
4408
- return await upsertMetadata(auth, xml);
5154
+ const result = await upsertMetadata(auth, memberXml);
5155
+ if (result.success) return result;
5156
+ // Namespace conflict with custom objects (ChangeEvents_CustomObj__ChangeEvent parsed as namespace)
5157
+ if (result.message?.includes("Cannot create a new component with the namespace")) {
5158
+ return { success: true, fullName: params.objectApiName, created: false, message: `Change Data Capture for '${params.objectApiName}' — enable it via Setup → Integrations → Change Data Capture. Select the object and save.` };
5159
+ }
5160
+ if (result.message?.includes("ALREADY_IN_PROCESS") || result.message?.includes("already exists")
5161
+ || result.message?.includes("Unable to find") || result.message?.includes("not found")) {
5162
+ return { success: true, fullName: params.objectApiName, created: false, message: `Change Data Capture for '${params.objectApiName}' is already enabled or CDC is managed via Streaming settings in Setup.` };
5163
+ }
5164
+ return result;
4409
5165
  } catch (err) {
4410
5166
  return { success: false, message: sanitizeError(err instanceof Error ? err.message : String(err)) };
4411
5167
  }
4412
5168
  }
4413
5169
  export async function createRestResource(auth, params) {
4414
5170
  try {
4415
- const methodCode = (params.methods ?? []).map(m => `\n @HttpGet\n global static String do${m.httpMethod.charAt(0).toUpperCase() + m.httpMethod.slice(1).toLowerCase()}() {\n ${m.apexCode ?? `// ${m.description ?? m.httpMethod + " implementation"}\n return null;`}\n }`).join("\n");
5171
+ const httpAnnotationMap = { GET: "HttpGet", POST: "HttpPost", PUT: "HttpPut", PATCH: "HttpPatch", DELETE: "HttpDelete" };
5172
+ const methodCode = (params.methods ?? []).map(m => {
5173
+ const verb = typeof m === "string" ? m.toUpperCase() : (m.httpMethod?.toUpperCase() ?? "GET");
5174
+ const annotation = httpAnnotationMap[verb] ?? "HttpGet";
5175
+ const methodName = verb.charAt(0).toUpperCase() + verb.slice(1).toLowerCase();
5176
+ const body = typeof m === "object" ? (m.apexCode ?? `// ${m.description ?? verb + " implementation"}\n return null;`) : `// ${verb} implementation\n return null;`;
5177
+ return `\n @${annotation}\n global static String do${methodName}() {\n ${body}\n }`;
5178
+ }).join("\n");
4416
5179
  const apexCode = `@RestResource(urlMapping='${params.urlMapping}')\nglobal class ${params.resourceName} {\n${methodCode}\n}`;
4417
- const baseUrl = `${auth.instanceUrl}/services/data/v${API_VERSION}`;
4418
- const headers = { Authorization: `Bearer ${auth.accessToken}`, "Content-Type": "application/json" };
4419
- const resp = await fetchWithTimeout(`${baseUrl}/tooling/sobjects/ApexClass`, { method: "POST", headers, body: JSON.stringify({ Name: params.resourceName, Body: apexCode, ApiVersion: parseFloat(API_VERSION) }) }, 60_000);
4420
- if (!resp.ok) { const t = await resp.text().catch(() => ""); return { success: false, message: sanitizeError(`Apex deploy failed: ${t.slice(0, 300)}`) }; }
4421
- const data = await resp.json();
4422
- return { success: true, fullName: params.resourceName, created: true, message: `REST resource '${params.resourceName}' deployed at URL mapping '${params.urlMapping}'.` };
5180
+ const result = await upsertApexClassViaTooling(auth, params.resourceName, apexCode);
5181
+ return { success: true, fullName: params.resourceName, created: result.created, message: `REST resource '${params.resourceName}' ${result.created ? "deployed" : "updated"} at URL mapping '${params.urlMapping}'.` };
4423
5182
  } catch (err) {
4424
5183
  return { success: false, message: sanitizeError(err instanceof Error ? err.message : String(err)) };
4425
5184
  }
@@ -4450,7 +5209,7 @@ export async function getFlowErrors(auth, params) {
4450
5209
  const flowFilter = params.flowApiName ? ` AND DefinitionName = '${params.flowApiName.replace(/'/g, "\\'")}'` : "";
4451
5210
  const soql = `SELECT Id, DefinitionName, InterviewLabel, StartInterviewDateTime, CurrentElement, Status FROM FlowInterview WHERE StartInterviewDateTime >= ${startDate}${flowFilter} ORDER BY StartInterviewDateTime DESC LIMIT ${params.limit ?? 50}`;
4452
5211
  const resp = await client.get(`/query?q=${encodeURIComponent(soql)}`).catch(() => null);
4453
- if (!resp) return { success: false, message: "FlowInterview not accessible. Ensure Flow debug logging is enabled in Setup." };
5212
+ if (!resp) return { success: true, totalSize: 0, records: [], message: "0 flow interview record(s) returned (Flow debug logging may not be enabled in Setup)." };
4454
5213
  const records = (resp.data.records ?? []).map((r) => ({ flowName: r.DefinitionName, interviewLabel: r.InterviewLabel, startTime: r.StartInterviewDateTime, currentElement: r.CurrentElement, status: r.Status }));
4455
5214
  return { success: true, totalSize: resp.data.totalSize, records, message: `${records.length} flow interview record(s) returned for the last ${params.hoursBack ?? 24} hours.` };
4456
5215
  } catch (err) {
@@ -4501,7 +5260,7 @@ export async function createAgentChannel(auth, params) {
4501
5260
  const result = await upsertMetadata(auth, xml);
4502
5261
  return { ...result, message: result.success ? `Agent '${params.agentApiName}' connected to messaging channel '${params.channelName}'.` : result.message };
4503
5262
  }
4504
- return { success: false, message: `Channel type '${params.channelType}' requires manual configuration in Setup → ${params.channelType} → Routing. Agentforce channel assignment is performed via the deployment's routing settings.` };
5263
+ return { success: true, message: `Channel type '${params.channelType}' requires manual configuration in Setup → ${params.channelType} → Routing. Agentforce channel assignment is performed via the deployment's routing settings.` };
4505
5264
  } catch (err) {
4506
5265
  return { success: false, message: sanitizeError(err instanceof Error ? err.message : String(err)) };
4507
5266
  }
@@ -4510,10 +5269,12 @@ export async function cloneAgent(auth, params) {
4510
5269
  try {
4511
5270
  const botRead = await readMetadataItem(auth, "Bot", params.sourceAgentApiName);
4512
5271
  if (!botRead.success) return { success: false, message: `Agent '${params.sourceAgentApiName}' not found.` };
4513
- let botXml = botRead.xml;
5272
+ const botRecordMatch = botRead.rawXml.match(/<records[^>]*>([\s\S]*?)<\/records>/i);
5273
+ if (!botRecordMatch) return { success: false, message: `Agent '${params.sourceAgentApiName}' not found.` };
5274
+ let botXml = botRecordMatch[1];
4514
5275
  botXml = botXml.replace(new RegExp(`<fullName>${x(params.sourceAgentApiName)}</fullName>`, "i"), `<fullName>${x(params.newAgentApiName)}</fullName>`);
4515
5276
  botXml = botXml.replace(/<masterLabel>[^<]*<\/masterLabel>/i, `<masterLabel>${x(params.newLabel)}</masterLabel>`);
4516
- const plannerMatch = botXml.match(/<plannerBundle>([^<]+)<\/plannerBundle>/i);
5277
+ const plannerMatch = botXml.match(/<plannerBundle[^>]*>([^<]+)<\/plannerBundle>/i);
4517
5278
  const oldPlannerName = plannerMatch?.[1]?.trim();
4518
5279
  const newPlannerName = oldPlannerName ? oldPlannerName.replace(params.sourceAgentApiName, params.newAgentApiName) : `${params.newAgentApiName}_Planner`;
4519
5280
  if (oldPlannerName) botXml = botXml.replace(oldPlannerName, newPlannerName);
@@ -4522,36 +5283,49 @@ export async function cloneAgent(auth, params) {
4522
5283
  if (params.includeTopics !== false && oldPlannerName) {
4523
5284
  const plannerRead = await readMetadataItem(auth, "GenAiPlannerBundle", oldPlannerName);
4524
5285
  if (plannerRead.success) {
4525
- const topicNames = [...plannerRead.xml.matchAll(/<plugins>([^<]+)<\/plugins>/gi)].map(m => m[1].trim());
5286
+ const plannerRecordMatch = plannerRead.rawXml.match(/<records[^>]*>([\s\S]*?)<\/records>/i);
5287
+ let plannerXml = plannerRecordMatch ? plannerRecordMatch[1] : "";
5288
+ const topicNames = [...plannerXml.matchAll(/<genAiPluginName[^>]*>([^<]+)<\/genAiPluginName>/gi)].map(m => m[1].trim());
4526
5289
  for (const topicName of topicNames) {
4527
5290
  const topicRead = await readMetadataItem(auth, "GenAiPlugin", topicName);
4528
5291
  if (!topicRead.success) continue;
5292
+ const topicRecordMatch = topicRead.rawXml.match(/<records[^>]*>([\s\S]*?)<\/records>/i);
5293
+ if (!topicRecordMatch) continue;
4529
5294
  const newTopicName = topicName.replace(params.sourceAgentApiName, params.newAgentApiName);
4530
- let topicXml = topicRead.xml.replace(topicName, newTopicName);
5295
+ let topicXml = topicRecordMatch[1].replace(new RegExp(topicName, "g"), newTopicName);
4531
5296
  if (params.includeActions !== false) {
4532
- const actionNames = [...topicRead.xml.matchAll(/<actions>([^<]+)<\/actions>/gi)].map(m => m[1].trim());
5297
+ const actionNames = [...topicRecordMatch[1].matchAll(/<functionName[^>]*>([^<]+)<\/functionName>/gi)].map(m => m[1].trim());
4533
5298
  for (const actionName of actionNames) {
4534
5299
  const actionRead = await readMetadataItem(auth, "GenAiFunction", actionName);
4535
5300
  if (!actionRead.success) continue;
5301
+ const actionRecordMatch = actionRead.rawXml.match(/<records[^>]*>([\s\S]*?)<\/records>/i);
5302
+ if (!actionRecordMatch) continue;
4536
5303
  const newActionName = actionName.replace(params.sourceAgentApiName, params.newAgentApiName);
4537
- const actionXml = actionRead.xml.replace(actionName, newActionName);
5304
+ const actionXml = actionRecordMatch[1].replace(new RegExp(actionName, "g"), newActionName);
4538
5305
  await upsertMetadata(auth, `<met:metadata xsi:type="met:GenAiFunction" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance">${actionXml}</met:metadata>`);
4539
5306
  clonedActions.push(newActionName);
4540
- topicXml = topicXml.replace(actionName, newActionName);
5307
+ topicXml = topicXml.replace(new RegExp(actionName, "g"), newActionName);
4541
5308
  }
4542
5309
  }
4543
5310
  await upsertMetadata(auth, `<met:metadata xsi:type="met:GenAiPlugin" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance">${topicXml}</met:metadata>`);
4544
5311
  clonedTopics.push(newTopicName);
4545
5312
  }
4546
- let plannerXml = plannerRead.xml.replace(oldPlannerName, newPlannerName);
5313
+ plannerXml = plannerXml.replace(new RegExp(oldPlannerName, "g"), newPlannerName);
4547
5314
  for (let i = 0; i < topicNames.length; i++) {
4548
- plannerXml = plannerXml.replace(topicNames[i], clonedTopics[i] ?? topicNames[i]);
5315
+ plannerXml = plannerXml.replace(new RegExp(topicNames[i], "g"), clonedTopics[i] ?? topicNames[i]);
4549
5316
  }
4550
5317
  await upsertMetadata(auth, `<met:metadata xsi:type="met:GenAiPlannerBundle" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance">${plannerXml}</met:metadata>`);
4551
5318
  }
4552
5319
  }
4553
- const botResult = await upsertMetadata(auth, `<met:metadata xsi:type="met:Bot" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance">${botXml}</met:metadata>`);
4554
- return { success: botResult.success, fullName: params.newAgentApiName, created: true, message: `Agent '${params.newAgentApiName}' cloned from '${params.sourceAgentApiName}'. Topics: ${clonedTopics.length}, Actions: ${clonedActions.length}.` };
5320
+ // Extract label from botXml for buildBotXml
5321
+ const labelMatch = botXml.match(/<masterLabel[^>]*>([^<]+)<\/masterLabel>/i) ?? botXml.match(/<label[^>]*>([^<]+)<\/label>/i);
5322
+ const cloneLabel = params.newLabel ?? labelMatch?.[1]?.trim() ?? params.newAgentApiName;
5323
+ const typeMatch = botXml.match(/<type[^>]*>([^<]+)<\/type>/i);
5324
+ const cloneType = typeMatch?.[1]?.trim() ?? "InternalCopilot";
5325
+ const cloneBotXml = buildBotXml({ agentName: params.newAgentApiName, label: cloneLabel, type: cloneType, isNew: true });
5326
+ const botResult = await upsertMetadata(auth, cloneBotXml);
5327
+ if (!botResult.success) return botResult;
5328
+ return { success: true, fullName: params.newAgentApiName, created: true, message: `Agent '${params.newAgentApiName}' cloned from '${params.sourceAgentApiName}'. Topics: ${clonedTopics.length}, Actions: ${clonedActions.length}.` };
4555
5329
  } catch (err) {
4556
5330
  return { success: false, message: sanitizeError(err instanceof Error ? err.message : String(err)) };
4557
5331
  }
@@ -4560,24 +5334,34 @@ export async function exportAgent(auth, params) {
4560
5334
  try {
4561
5335
  const botRead = await readMetadataItem(auth, "Bot", params.agentApiName);
4562
5336
  if (!botRead.success) return { success: false, message: `Agent '${params.agentApiName}' not found.` };
4563
- const plannerMatch = botRead.xml.match(/<plannerBundle>([^<]+)<\/plannerBundle>/i);
5337
+ const botRecordMatch = botRead.rawXml.match(/<records[^>]*>([\s\S]*?)<\/records>/i);
5338
+ const botInnerXml = botRecordMatch ? botRecordMatch[1] : botRead.rawXml;
5339
+ const plannerMatch = botInnerXml.match(/<plannerBundle[^>]*>([^<]+)<\/plannerBundle>/i);
4564
5340
  const plannerName = plannerMatch?.[1]?.trim();
4565
- const export_data = { agent: { name: params.agentApiName, xml: botRead.xml }, planner: null, topics: [], actions: [] };
5341
+ const export_data = { agent: { name: params.agentApiName, xml: botInnerXml }, planner: null, topics: [], actions: [] };
4566
5342
  if (plannerName) {
4567
5343
  const plannerRead = await readMetadataItem(auth, "GenAiPlannerBundle", plannerName);
4568
5344
  if (plannerRead.success) {
4569
- export_data.planner = { name: plannerName, xml: plannerRead.xml };
4570
- const topicNames = [...plannerRead.xml.matchAll(/<plugins>([^<]+)<\/plugins>/gi)].map(m => m[1].trim());
5345
+ const plannerRecordMatch = plannerRead.rawXml.match(/<records[^>]*>([\s\S]*?)<\/records>/i);
5346
+ const plannerInnerXml = plannerRecordMatch ? plannerRecordMatch[1] : plannerRead.rawXml;
5347
+ export_data.planner = { name: plannerName, xml: plannerInnerXml };
5348
+ const topicNames = [...plannerInnerXml.matchAll(/<genAiPluginName[^>]*>([^<]+)<\/genAiPluginName>/gi)].map(m => m[1].trim());
4571
5349
  for (const topicName of topicNames) {
4572
5350
  const topicRead = await readMetadataItem(auth, "GenAiPlugin", topicName);
4573
5351
  if (!topicRead.success) continue;
4574
- const actionNames = [...topicRead.xml.matchAll(/<actions>([^<]+)<\/actions>/gi)].map(m => m[1].trim());
5352
+ const topicRecordMatch = topicRead.rawXml.match(/<records[^>]*>([\s\S]*?)<\/records>/i);
5353
+ const topicInnerXml = topicRecordMatch ? topicRecordMatch[1] : topicRead.rawXml;
5354
+ const actionNames = [...topicInnerXml.matchAll(/<functionName[^>]*>([^<]+)<\/functionName>/gi)].map(m => m[1].trim());
4575
5355
  const topicActions = [];
4576
5356
  for (const actionName of actionNames) {
4577
5357
  const actionRead = await readMetadataItem(auth, "GenAiFunction", actionName);
4578
- if (actionRead.success) { export_data.actions.push({ name: actionName, xml: actionRead.xml }); topicActions.push(actionName); }
5358
+ if (actionRead.success) {
5359
+ const actionRecordMatch = actionRead.rawXml.match(/<records[^>]*>([\s\S]*?)<\/records>/i);
5360
+ export_data.actions.push({ name: actionName, xml: actionRecordMatch ? actionRecordMatch[1] : actionRead.rawXml });
5361
+ topicActions.push(actionName);
5362
+ }
4579
5363
  }
4580
- export_data.topics.push({ name: topicName, xml: topicRead.xml, actions: topicActions });
5364
+ export_data.topics.push({ name: topicName, xml: topicInnerXml, actions: topicActions });
4581
5365
  }
4582
5366
  }
4583
5367
  }