@dypai-ai/mcp 1.7.4 → 1.7.5
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/package.json +1 -1
- package/src/tools/sync/planner.js +57 -0
- package/src/tools/sync/pull.js +44 -17
package/package.json
CHANGED
|
@@ -662,6 +662,39 @@ function localToComparable(entry, mapsCtx) {
|
|
|
662
662
|
return remoteToComparable(row, mapsCtx)
|
|
663
663
|
}
|
|
664
664
|
|
|
665
|
+
export function flowSourceHashesMatch(entry, remoteRow) {
|
|
666
|
+
if (entry?.source !== "flow" || !entry.pushPayload || !remoteRow) return false
|
|
667
|
+
const localHash = entry.pushPayload.workflow_code?.metadata?.source_hash
|
|
668
|
+
const remoteHash = remoteRow.workflow_code?.metadata?.source_hash
|
|
669
|
+
return typeof localHash === "string"
|
|
670
|
+
&& localHash.length > 0
|
|
671
|
+
&& localHash === remoteHash
|
|
672
|
+
}
|
|
673
|
+
|
|
674
|
+
export function preserveMatchingRemoteFileRefs(rawLocalDoc, localComparable, remoteComparable) {
|
|
675
|
+
if (!rawLocalDoc || !localComparable || !remoteComparable) return localComparable
|
|
676
|
+
if (Array.isArray(rawLocalDoc)) {
|
|
677
|
+
if (!Array.isArray(localComparable) || !Array.isArray(remoteComparable)) return localComparable
|
|
678
|
+
rawLocalDoc.forEach((item, index) => {
|
|
679
|
+
preserveMatchingRemoteFileRefs(item, localComparable[index], remoteComparable[index])
|
|
680
|
+
})
|
|
681
|
+
return localComparable
|
|
682
|
+
}
|
|
683
|
+
if (typeof rawLocalDoc !== "object" || Array.isArray(localComparable) || Array.isArray(remoteComparable)) {
|
|
684
|
+
return localComparable
|
|
685
|
+
}
|
|
686
|
+
|
|
687
|
+
for (const [key, value] of Object.entries(rawLocalDoc)) {
|
|
688
|
+
if (key.endsWith("_file") && typeof value === "string" && remoteComparable[key] === value) {
|
|
689
|
+
delete localComparable[key.replace(/_file$/, "")]
|
|
690
|
+
localComparable[key] = value
|
|
691
|
+
continue
|
|
692
|
+
}
|
|
693
|
+
preserveMatchingRemoteFileRefs(value, localComparable[key], remoteComparable[key])
|
|
694
|
+
}
|
|
695
|
+
return localComparable
|
|
696
|
+
}
|
|
697
|
+
|
|
665
698
|
function deepEqual(a, b) {
|
|
666
699
|
if (a === b) return true
|
|
667
700
|
if (typeof a !== typeof b) return false
|
|
@@ -769,6 +802,7 @@ export function detectConflicts(stateSnapshot, remoteByName) {
|
|
|
769
802
|
export function computePlan(local, remote, mapsCtx, { deleteOrphans = false, stateSnapshot = null, liveRemote = null } = {}) {
|
|
770
803
|
const plan = { create: [], update: [], delete: [], unchanged: [] }
|
|
771
804
|
const warnings = []
|
|
805
|
+
const compilerOnlyDrift = []
|
|
772
806
|
|
|
773
807
|
const endpointNameCollisions = findEndpointNameCollisions(Object.keys(remote?.byName || {}))
|
|
774
808
|
if (endpointNameCollisions.length) {
|
|
@@ -822,6 +856,19 @@ export function computePlan(local, remote, mapsCtx, { deleteOrphans = false, sta
|
|
|
822
856
|
}
|
|
823
857
|
|
|
824
858
|
const remoteCanonical = remoteToComparable(remoteRow, mapsCtx)
|
|
859
|
+
if (entry.doc) {
|
|
860
|
+
preserveMatchingRemoteFileRefs(entry.doc, localCanonical, remoteCanonical)
|
|
861
|
+
}
|
|
862
|
+
if (
|
|
863
|
+
flowSourceHashesMatch(entry, remoteRow)
|
|
864
|
+
&& !deepEqual(localCanonical.workflow, remoteCanonical.workflow)
|
|
865
|
+
) {
|
|
866
|
+
// Same authored source, different compiler artifact (for example after
|
|
867
|
+
// a compiler upgrade). Keep comparing roles/contracts/description, but
|
|
868
|
+
// do not stage a mass endpoint update solely to rewrite generated IR.
|
|
869
|
+
localCanonical.workflow = remoteCanonical.workflow
|
|
870
|
+
compilerOnlyDrift.push(name)
|
|
871
|
+
}
|
|
825
872
|
if (deepEqual(localCanonical, remoteCanonical)) {
|
|
826
873
|
plan.unchanged.push(name)
|
|
827
874
|
} else {
|
|
@@ -830,6 +877,16 @@ export function computePlan(local, remote, mapsCtx, { deleteOrphans = false, sta
|
|
|
830
877
|
}
|
|
831
878
|
}
|
|
832
879
|
|
|
880
|
+
if (compilerOnlyDrift.length) {
|
|
881
|
+
warnings.push({
|
|
882
|
+
type: "compiler_artifact_drift_ignored",
|
|
883
|
+
count: compilerOnlyDrift.length,
|
|
884
|
+
examples: compilerOnlyDrift.slice(0, 25),
|
|
885
|
+
truncated: compilerOnlyDrift.length > 25,
|
|
886
|
+
hint: "Authored Flow source_hash matches live. Generated workflow differences from a compiler upgrade were not staged.",
|
|
887
|
+
})
|
|
888
|
+
}
|
|
889
|
+
|
|
833
890
|
if (deleteOrphans) {
|
|
834
891
|
for (const name of Object.keys(remote.byName)) {
|
|
835
892
|
if (!local.byName[name]) plan.delete.push({ name })
|
package/src/tools/sync/pull.js
CHANGED
|
@@ -761,10 +761,15 @@ export const dypaiPullTool = {
|
|
|
761
761
|
description: "If true, removes existing endpoints/, sql/, prompts/ before writing and allows persisted Flow source to overwrite differing local .flow.ts files.",
|
|
762
762
|
default: false,
|
|
763
763
|
},
|
|
764
|
+
live_authoritative: {
|
|
765
|
+
type: "boolean",
|
|
766
|
+
description: "Recovery mode. Export every live workflow as legacy YAML instead of trusting persisted Flow/Automation source. Use only when live is intentionally authoritative and source has drifted.",
|
|
767
|
+
default: false,
|
|
768
|
+
},
|
|
764
769
|
},
|
|
765
770
|
},
|
|
766
771
|
|
|
767
|
-
async execute({ project_id, out_dir = "./dypai", clean = false } = {}) {
|
|
772
|
+
async execute({ project_id, out_dir = "./dypai", clean = false, live_authoritative = false } = {}) {
|
|
768
773
|
const { path: outDir, source: outDirSource } = resolveOutDir(out_dir)
|
|
769
774
|
const suspiciousWarning = suspiciousPathWarning(outDir, outDirSource)
|
|
770
775
|
|
|
@@ -864,6 +869,7 @@ export const dypaiPullTool = {
|
|
|
864
869
|
|
|
865
870
|
const filesWritten = []
|
|
866
871
|
const errors = []
|
|
872
|
+
const sourceRecoveryWarnings = []
|
|
867
873
|
|
|
868
874
|
const capabilityGroups = capabilityCatalogResult?.groups || []
|
|
869
875
|
const capabilityDoc = {
|
|
@@ -939,7 +945,7 @@ export const dypaiPullTool = {
|
|
|
939
945
|
row.workflow_source = parseMaybeJson(row.workflow_source)
|
|
940
946
|
try {
|
|
941
947
|
const workflowSource = row.workflow_source
|
|
942
|
-
if (isEditableTsSource(workflowSource)) {
|
|
948
|
+
if (isEditableTsSource(workflowSource) && !live_authoritative) {
|
|
943
949
|
const groupName = row.group_id ? (mapsCtx.groupIdToName[row.group_id] || null) : null
|
|
944
950
|
const sourceFile = normalizeEditableSourceFile(workflowSource, row.name)
|
|
945
951
|
const relFlowPath = flowSourceRelPath(sourceFile)
|
|
@@ -985,18 +991,30 @@ export const dypaiPullTool = {
|
|
|
985
991
|
continue
|
|
986
992
|
}
|
|
987
993
|
|
|
988
|
-
|
|
989
|
-
|
|
990
|
-
|
|
991
|
-
|
|
992
|
-
|
|
993
|
-
|
|
994
|
+
// The editable TS source has been lost, but live still contains the
|
|
995
|
+
// complete executable workflow. Recover it as legacy YAML so a live
|
|
996
|
+
// repair pull remains complete and its state baseline stays honest.
|
|
997
|
+
// The warning makes the source loss visible without dropping the
|
|
998
|
+
// endpoint from disk or producing a false clean dry-run.
|
|
999
|
+
sourceRecoveryWarnings.push({
|
|
994
1000
|
endpoint: row.name,
|
|
995
|
-
|
|
1001
|
+
source_file: sourceFile,
|
|
1002
|
+
recovered_as: "legacy_yaml",
|
|
1003
|
+
warning:
|
|
996
1004
|
`Endpoint is source-authored but ${sourceFile} was not found in persisted project source. ` +
|
|
997
|
-
|
|
1005
|
+
"Recovered the live workflow as legacy YAML; restore editable TypeScript later if desired.",
|
|
998
1006
|
})
|
|
999
|
-
|
|
1007
|
+
row.workflow_source = null
|
|
1008
|
+
}
|
|
1009
|
+
|
|
1010
|
+
if (isEditableTsSource(workflowSource) && live_authoritative) {
|
|
1011
|
+
sourceRecoveryWarnings.push({
|
|
1012
|
+
endpoint: row.name,
|
|
1013
|
+
source_file: normalizeEditableSourceFile(workflowSource, row.name),
|
|
1014
|
+
recovered_as: "legacy_yaml",
|
|
1015
|
+
warning: "Live-authoritative recovery exported the executable workflow as legacy YAML. The previous editable TypeScript source remains available in the backup/history.",
|
|
1016
|
+
})
|
|
1017
|
+
row.workflow_source = null
|
|
1000
1018
|
}
|
|
1001
1019
|
|
|
1002
1020
|
const { doc, extractedFiles } = serializeEndpoint(row, mapsCtx)
|
|
@@ -1191,12 +1209,26 @@ export const dypaiPullTool = {
|
|
|
1191
1209
|
: ["Read dypai/schema.sql before writing queries.", "Edit Flow in dypai/flows/, Automations in dypai/automations/, or legacy YAML in dypai/endpoints/, then dypai_diff → dypai_push."],
|
|
1192
1210
|
}
|
|
1193
1211
|
|
|
1212
|
+
const safetyWarnings = [
|
|
1213
|
+
...(endpointNameCollisions.length ? [{
|
|
1214
|
+
type: "canonical_endpoint_name_collisions",
|
|
1215
|
+
count: endpointNameCollisions.length,
|
|
1216
|
+
examples: endpointNameCollisions.slice(0, 10),
|
|
1217
|
+
hint: "Legacy underscore and canonical hyphen slugs coexist. Treat them as separate live APIs; audit traffic and migrate/delete deliberately.",
|
|
1218
|
+
}] : []),
|
|
1219
|
+
...sourceRecoveryWarnings.map((item) => ({
|
|
1220
|
+
type: "missing_editable_source_recovered",
|
|
1221
|
+
...item,
|
|
1222
|
+
})),
|
|
1223
|
+
]
|
|
1224
|
+
|
|
1194
1225
|
return {
|
|
1195
1226
|
success: errors.length === 0,
|
|
1196
1227
|
endpoints: endpoints.length,
|
|
1197
1228
|
files_written: filesWritten.length,
|
|
1198
1229
|
files_kept_local: keptLocalEndpoints.length,
|
|
1199
1230
|
kept_local_endpoints: keptLocalEndpoints.length ? keptLocalEndpoints : undefined,
|
|
1231
|
+
source_recovery_warnings: sourceRecoveryWarnings.length ? sourceRecoveryWarnings : undefined,
|
|
1200
1232
|
name_collisions: endpointNameCollisions.length,
|
|
1201
1233
|
output_dir: outDir,
|
|
1202
1234
|
out_dir_resolved_via: outDirSource,
|
|
@@ -1206,12 +1238,7 @@ export const dypaiPullTool = {
|
|
|
1206
1238
|
overview,
|
|
1207
1239
|
errors: errors.length ? errors : undefined,
|
|
1208
1240
|
warning: suspiciousWarning || undefined,
|
|
1209
|
-
safety_warnings:
|
|
1210
|
-
type: "canonical_endpoint_name_collisions",
|
|
1211
|
-
count: endpointNameCollisions.length,
|
|
1212
|
-
examples: endpointNameCollisions.slice(0, 10),
|
|
1213
|
-
hint: "Legacy underscore and canonical hyphen slugs coexist. Treat them as separate live APIs; audit traffic and migrate/delete deliberately.",
|
|
1214
|
-
}] : undefined,
|
|
1241
|
+
safety_warnings: safetyWarnings.length ? safetyWarnings : undefined,
|
|
1215
1242
|
hint: errors.length
|
|
1216
1243
|
? keptLocalEndpoints.length
|
|
1217
1244
|
? `${keptLocalEndpoints.length} local Flow file(s) differed and were kept. They were excluded from .dypai/state.json, so a later push will continue to block on remote drift. Review the files or rerun with clean:true only when remote is intentionally authoritative.`
|